{"owner":"JamieMason","repo":"syncpack","hasSkills":true,"totalSkillsCount":15,"totalTokensCount":20300,"categories":["anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":[".claude/rules/syncpack.md",".claude/skills/add-feature/SKILL.md",".claude/skills/add-feature/adding-command.md",".claude/skills/add-feature/adding-instance-state.md",".claude/skills/document-code/SKILL.md",".claude/skills/fix-bug/SKILL.md",".claude/skills/front-loading/SKILL.md",".claude/skills/optimize-benchmarks/SKILL.md",".claude/skills/review-docs/SKILL.md",".claude/skills/search-code/SKILL.md",".claude/skills/signal-over-noise/SKILL.md",".claude/skills/testing-gaps/SKILL.md",".claude/skills/write-code/SKILL.md",".claude/skills/write-tests/SKILL.md",".claude/skills/write-tests/patterns.md"],"skills":{".claude/rules/syncpack.md":"# Syncpack\n\nRust CLI tool for synchronising npm dependency versions across JavaScript monorepos.\n\n## Getting Started\n\n1. Run `just` to see available commands\n2. Read `.notes/` for architecture and mental model\n3. Check `skills/` for task-specific guides:\n\n## Communication Style\n\n- Signal over noise\n- Extremely concise. Sacrifice grammar for brevity\n- Action-oriented: what to DO\n- Grounded in facts: cite code/docs\n- BANNED WORDS: basically, essentially, in order to, comprehensive\n- Always use AskUserQuestion when asking questions\n\n## Pre-Implementation Checklist\n\nBefore large changes, answer YES to all:\n\n- Have I identified ALL decision points?\n- Have I listed trade-offs for each approach?\n- Have I asked user which strategy to use?\n- Am I NOT making architectural assumptions?\n- Am I NOT creating files user didn't request?\n\n## Hard Rules: Ask First\n\nMUST ask when:\n\n- User intent unclear or multiple valid approaches exist\n- Breaking changes or core architecture modifications\n- Creating ANY new files/modules not explicitly requested\n- Large refactors (identify ALL decision points first)\n\nMUST NOT:\n\n- Use banned words\n- Assume user wants plan or summary documentation\n- Refactor without asking\n- Make architectural decisions unilaterally\n\n## Proceed Without Asking\n\n- Pattern clearly exists in codebase\n- Following established convention\n- Non-breaking changes\n- Adding tests, fixing obvious bugs\n\n## Documentation Rules: MANDATORY\n\n**NEVER create new documentation files unless explicitly requested.**\n\nExamples of BANNED actions:\n\n- Creating \"status\" docs, \"validation\" docs, \"current-status\" files\n- Creating \"summary\" docs, \"guide\" docs, \"checklist\" files\n- Splitting existing docs into multiple files\n- Creating \"handoff\" docs, \"next steps\" docs\n\n**ONLY update existing docs:**\n\n- Update the original document in place\n- Add sections/checkboxes to existing files\n- Mark tasks complete where they already exist\n\n**If user has ONE plan file, keep ONE plan file.**\n\nDo NOT proliferate documentation. Update existing files only.\n",".claude/skills/add-feature/SKILL.md":"---\nname: add-feature\ndescription: Add new features to Syncpack including commands and validation logic. Use when implementing new CLI commands, adding InstanceState variants, or extending version group behaviour.\n---\n\n# Add Feature\n\nGuide for adding new functionality to Syncpack.\n\n## Quick Decision\n\nWhat are you adding?\n\n- **New CLI command** (lint, fix, format, etc.) → See [adding-command.md](adding-command.md)\n- **New validation logic** (InstanceState variant) → See [adding-instance-state.md](adding-instance-state.md)\n- **Both** → Start with instance state, then add command\n\n## Prerequisites\n\nBefore adding any feature:\n\n1. Understand the 3-phase pattern: Create Context → Inspect Context → Run Command\n2. Know which phase your feature affects\n3. Check if similar code exists (use `ast-grep -p 'PATTERN' src/`)\n\n## Feature Types\n\n### Commands\n\nCommands are user-facing operations (`syncpack lint`, `syncpack fix`, etc.).\n\n**When to create a new command:**\n\n- New user-facing operation that doesn't fit existing commands\n- Different output format or behaviour needed\n\n**Registration points (all three required):**\n\n1. `src/cli.rs` - Add to `Subcommand` enum\n2. `src/main.rs` - Add match arm for dispatch\n3. `src/commands/*.rs` - Implement `pub fn run(ctx: Context) -> i32`\n\n→ Full guide: [adding-command.md](adding-command.md)\n\n### Instance States\n\nInstanceState variants describe validation results (valid, fixable, unfixable, suspect).\n\n**When to add a new state:**\n\n- New validation rule needed\n- New type of error/warning to report\n- New auto-fix capability\n\n**Location:** `src/instance_state.rs` + `src/visit_packages/*.rs`\n\n→ Full guide: [adding-instance-state.md](adding-instance-state.md)\n\n## Common Workflow\n\n1. Write failing test using TestBuilder\n2. Implement minimal code to pass\n3. Run `cargo clippy` and fix warnings\n4. Refactor if needed\n5. Update documentation\n\n## Checklist\n\nBefore submitting:\n\n- [ ] Tests pass (`just test`)\n- [ ] Zero clippy warnings\n- [ ] Follows existing patterns\n- [ ] Registered in all required places (for commands)\n- [ ] Uses TDD approach\n",".claude/skills/add-feature/adding-command.md":"# Example: Adding a New Command\n\n<scenario>\nThis guide walks through adding a new `syncpack check` command that lists outdated dependencies without modifying files.\n</scenario>\n\n<step number=\"1\">\n\n## Step 1: Decide on the Visitor\n\n**Question:** Does this command deal with dependency versions or package.json formatting?\n\n**Answer:** Dependency versions → Use `visit_packages`\n\n</step>\n\n<step number=\"2\">\n\n## Step 2: Add Command Enum Variant\n\n**File:** `src/cli.rs`\n\n```rust\npub enum Subcommand {\n  Lint,\n  Fix,\n  Format,\n  Update,\n  List,\n  Json,\n  Check,  // ← Add this\n}\n```\n\n</step>\n\n<step number=\"3\">\n\n## Step 3: Create Command Implementation\n\n**File:** `src/commands/check.rs`\n\n<implementation>\n\n```rust\nuse crate::{commands::ui, context::Context};\n\n/// List outdated dependencies without modifying files\npub fn run(ctx: Context) -> i32 {\n  let mut has_outdated = false;\n\n  ctx.version_groups.iter().for_each(|group| {\n    let mut has_printed_group = false;\n\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n      let mut has_printed_dependency = false;\n\n      dependency\n        .get_sorted_instances()\n        .filter(|instance| instance.is_outdated())\n        .for_each(|instance| {\n          // Lazy print headers only when we find outdated instances\n          if !has_printed_group {\n            ui::group::print_header(&ctx, group);\n            has_printed_group = true;\n          }\n          if !has_printed_dependency {\n            ui::dependency::print(&ctx, dependency, &group.variant);\n            has_printed_dependency = true;\n          }\n\n          // Print the outdated instance\n          if ctx.config.cli.show_instances {\n            ui::instance::print(&ctx, instance, &group.variant);\n          }\n\n          has_outdated = true;\n        });\n    });\n  });\n\n  if has_outdated {\n    1  // Exit with error code\n  } else {\n    ui::util::print_no_issues_found();\n    0  // Exit successfully\n  }\n}\n```\n\n</implementation>\n\n<pattern_notes>\n\n**Pattern notes:**\n\n- Take ownership of `Context`\n- Return `i32` exit code (0 = success, 1 = failure)\n- Use lazy printing (only print headers when needed)\n- Iterate: version groups → dependencies → instances\n- Filter instances by state (`.filter(|instance| instance.is_outdated())`)\n- Use existing UI utilities for consistent output\n\n</pattern_notes>\n\n</step>\n\n<step number=\"4\">\n\n## Step 4: Register Command in Module\n\n**File:** `src/commands.rs`\n\n```rust\npub mod fix;\npub mod format;\npub mod json;\npub mod lint;\npub mod list;\npub mod update;\npub mod check;  // ← Add this\n```\n\n</step>\n\n<step number=\"5\">\n\n## Step 5: Add to Main Dispatch\n\n**File:** `src/main.rs`\n\n```rust\nuse {\n  crate::{\n    commands::{check, fix, format, json, lint, list, update},  // ← Add check\n    // ... other imports\n  },\n};\n\n// ... in main() function:\n\nlet _exit_code = match ctx.config.cli.subcommand {\n  Subcommand::Fix => {\n    let ctx = visit_packages(ctx);\n    fix::run(ctx)\n  }\n  Subcommand::Format => {\n    let ctx = visit_formatting(ctx);\n    format::run(ctx)\n  }\n  Subcommand::Lint => {\n    let ctx = visit_packages(ctx);\n    lint::run(ctx)\n  }\n  Subcommand::Update => {\n    let mut ctx = ctx;\n    ctx.fetch_all_updates().await;\n    let ctx = visit_packages(ctx);\n    update::run(ctx)\n  }\n  Subcommand::List => {\n    let ctx = visit_packages(ctx);\n    list::run(ctx)\n  }\n  Subcommand::Json => {\n    let ctx = visit_packages(ctx);\n    json::run(ctx)\n  }\n  Subcommand::Check => {  // ← Add this\n    let ctx = visit_packages(ctx);\n    check::run(ctx)\n  }\n};\n```\n\n<key_points>\n\n**Key points:**\n\n- Must call `visit_packages(ctx)` before command (to assign InstanceStates)\n- Pass the returned Context to your command's run function\n- The command consumes Context and returns exit code\n\n</key_points>\n\n</step>\n\n<step number=\"6\">\n\n## Step 6: Add CLI Help Text (Optional)\n\n**File:** `src/cli.rs`\n\n```rust\n#[derive(Debug, clap::Parser)]\n#[command(name = \"syncpack\")]\n#[command(about = \"Manage multiple package.json files\")]\npub struct Cli {\n  #[command(subcommand)]\n  pub subcommand: Subcommand,\n  // ... other fields\n}\n\n#[derive(Debug, clap::Subcommand)]\npub enum Subcommand {\n  /// Find and report outdated dependencies\n  #[command(name = \"check\")]\n  Check,\n  // ... other variants\n}\n```\n\n</step>\n\n<step number=\"7\">\n\n## Step 7: Write Tests\n\n**File:** `src/commands/check_test.rs` (or add to existing test file)\n\n<test_examples>\n\n```rust\nuse {\n  crate::{\n    commands::check,\n    instance_state::{FixableInstance::*, InstanceState, ValidInstance::*},\n    test::{builder::TestBuilder, expect::ExpectedInstance},\n  },\n  serde_json::json,\n};\n\n#[test]\nfn reports_outdated_dependencies() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"17.0.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"17.0.0\", \"17.0.1\", \"18.0.0\", \"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  // Verify the instance is marked as outdated\n  let react_instance = ctx.instances.iter()\n    .find(|i| i.dependency.name == \"react\")\n    .expect(\"Should find react instance\");\n\n  assert!(react_instance.is_outdated());\n\n  // Run the command (in real test, you'd capture stdout)\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 1, \"Should exit with error when outdated found\");\n}\n\n#[test]\nfn exits_successfully_when_up_to_date() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"18.2.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 0, \"Should exit successfully when up to date\");\n}\n```\n\n</test_examples>\n\n</step>\n\n<step number=\"8\">\n\n## Step 8: Test Locally\n\n```bash\n# Build and run\ncargo run -- check --help\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- check\n\n# Run tests\ncargo test check\njust test\n```\n\n</step>\n\n<step number=\"9\">\n\n## Step 9: Verify All Places Updated\n\n<checklist>\n\n- [x] Added to `Subcommand` enum in `src/cli.rs`\n- [x] Created `src/commands/check.rs` with `pub fn run(ctx: Context) -> i32`\n- [x] Registered in `src/commands.rs` module\n- [x] Added match arm in `src/main.rs`\n- [x] Called appropriate visitor (`visit_packages`)\n- [x] Written tests\n- [x] Tested locally\n\n</checklist>\n\n</step>\n\n<variations>\n\n## Common Variations\n\n<variation type=\"file_modification\">\n\n### Command that modifies files\n\n```rust\npub fn run(mut ctx: Context) -> i32 {\n  // ... iterate and modify instances\n\n  // Write changes to disk\n  ctx.packages.write_all();\n\n  0\n}\n```\n\n</variation>\n\n<variation type=\"registry\">\n\n### Command that needs registry data\n\nIn `main.rs`:\n\n```rust\nSubcommand::MyCommand => {\n  let mut ctx = ctx;\n  ctx.fetch_all_updates().await;  // Fetch from npm\n  let ctx = visit_packages(ctx);\n  my_command::run(ctx)\n}\n```\n\n</variation>\n\n<variation type=\"cli_options\">\n\n### Command with specific CLI options\n\n```rust\npub enum Subcommand {\n  #[command(name = \"check\")]\n  Check {\n    /// Only check production dependencies\n    #[arg(long)]\n    prod_only: bool,\n  },\n}\n```\n\nAccess in command:\n\n```rust\nif let Subcommand::Check { prod_only } = &ctx.config.cli.subcommand {\n  if *prod_only {\n    // Filter logic\n  }\n}\n```\n\n</variation>\n\n</variations>\n\n<troubleshooting>\n\n## Troubleshooting\n\n<problem>\n**Problem:** Command not found\n→ Check you added it to all three places: enum, main.rs match, commands module\n</problem>\n\n<problem>\n**Problem:** Instances have `Unknown` state\n→ You forgot to call `visit_packages(ctx)` before your command\n</problem>\n\n<problem>\n**Problem:** Compilation error about moved value\n→ Make sure your command signature is `pub fn run(ctx: Context) -> i32`\n</problem>\n\n<problem>\n**Problem:** Changes not persisting\n→ You need to call `ctx.packages.write_all()` in commands that modify files\n</problem>\n\n</troubleshooting>\n\n<related_examples>\n\n## Related Examples\n\n- See `src/commands/lint.rs` for read-only command\n- See `src/commands/fix.rs` for file-modifying command\n- See `src/commands/update.rs` for command using registry client\n\n</related_examples>\n",".claude/skills/add-feature/adding-instance-state.md":"# Adding a New InstanceState Variant\n\n<scenario>\nThis guide walks through adding new `InstanceState` variants. The example used\nthroughout is `SameMinorHasMajorMismatch`, a real variant added during the\nsameMinor improvement work. It is an `Unfixable` state assigned when instances\nin a `sameMinor` version group have different MAJOR versions.\n</scenario>\n\n---\n\n## Step 1: Choose the right category\n\n```\nInstanceState\n├── Unknown              — never assign this; it is the initial pre-inspection state\n├── Valid(ValidInstance) — instance follows all rules correctly\n├── Invalid(InvalidInstance)\n│   ├── Fixable(FixableInstance)                  — syncpack knows the correct value and can auto-fix\n│   ├── Unfixable(UnfixableInstance)              — ambiguous; needs human decision\n│   └── Conflict(SemverGroupAndVersionConflict)   — two active rules are simultaneously unsatisfiable\n└── Suspect(SuspectInstance)                      — user misconfiguration\n```\n\nAsk:\n\n- Do we know the correct value? → `Fixable`\n- Is the situation ambiguous, multiple valid options? → `Unfixable`\n- Are two rules active and irreconcilable? → `Conflict`\n- Has the user misconfigured something? → `Suspect`\n- Is everything fine? → `Valid`\n\nFor `SameMinorHasMajorMismatch`: crossing a major version boundary is unsafe\nand we cannot pick a winner without asking the user → `Unfixable`.\n\n---\n\n## Step 2: Add the variant to `src/instance_state.rs`\n\nFind the appropriate enum and add the variant with a multi-line doc comment.\nEvery condition relevant to the state must be listed. Use these symbols:\n\n- `✓` — condition is true / requirement is met\n- `✘` — condition is false / requirement is violated\n- `?` — ambiguous / unknown / why we cannot auto-fix\n- `!` — notable side effect or policy decision\n\n```rust\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum UnfixableInstance {\n  // ... existing variants ...\n\n  /// - ✘ Instance is in a sameMinor version group\n  /// - ✘ One or more other instances have a different MAJOR version\n  /// - ? Crossing a major version boundary is unsafe\n  /// - ? We cannot know which MAJOR the user wants and have to ask them\n  SameMinorHasMajorMismatch,\n}\n```\n\nThe doc comment is the source of truth for when this state is assigned. Be\nprecise — every bullet should be independently verifiable from the visitor code.\n\n---\n\n## Step 3: Check whether any `impl` blocks need updating\n\nMost additions require no changes to `impl InstanceState`. The severity, the\n`is_fixable()` / `is_unfixable()` helpers, and the `get_name()` / `get_status_type()`\nmethods all work by matching on the outer category (`Valid`, `Invalid`,\n`Suspect`), not on individual variants.\n\nThe only time you need to touch `impl InstanceState` is if you are adding a\nvariant that needs a **custom severity** (rare) or a new top-level predicate\n(e.g. `is_banned()`). For ordinary additions, no changes are needed.\n\n---\n\n## Step 4: Write the detection logic in the visitor\n\nVisitor files live in `src/visit_packages/`. Each visitor receives a\n`&Dependency` and must call one of the mark methods on every instance:\n\n```rust\ninstance.mark_valid(ValidInstance::SatisfiesSameMinorGroup, specifier);\ninstance.mark_fixable(FixableInstance::DiffersToPin, expected_specifier);\ninstance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\ninstance.mark_conflict(SemverGroupAndVersionConflict::MatchConflictsWithLocal);\ninstance.mark_suspect(SuspectInstance::InvalidLocalVersion);\n```\n\nNever assign to `instance.state` directly — always use these methods.\n\nThe specifier argument to `mark_valid` and `mark_fixable` is the **expected**\nspecifier — what the instance should be set to. Read the actual specifier from\n`instance.descriptor.specifier`.\n\nExample — marking all instances when a major mismatch is detected (computed\nonce before the per-instance loop):\n\n```rust\nlet has_major_mismatch = dependency\n    .instances\n    .iter()\n    .any(|i| !i.already_has_same_major_as_all(&dependency.instances));\n\ndependency.instances.iter().for_each(|instance| {\n    let actual_specifier = &instance.descriptor.specifier;\n\n    if has_major_mismatch {\n        instance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\n        return;\n    }\n\n    // ... rest of visitor logic ...\n});\n```\n\nKey rules:\n\n- **Every instance must be marked** before the visitor returns. No instance may\n  remain in `InstanceState::Unknown` after `visit_packages()` completes.\n- **Compute per-dependency values once** before the loop (e.g. highest\n  specifier, major-mismatch boolean) to avoid O(n²) re-checking.\n- **Do not read `instance.state` inside the visitor** to decide what to do —\n  states are write-once during inspection.\n\n---\n\n## Step 5: Integrate into `visit_packages.rs` (only for new visitors)\n\nIf you are adding a variant to an **existing** visitor, skip this step.\n\nIf you are adding a **new visitor module**, register it in\n`src/visit_packages.rs`:\n\n```rust\nmod banned;\nmod ignored;\nmod indent;\nmod pinned;\nmod preferred_semver;\nmod same_minor;   // ← example of an existing entry\nmod same_range;\nmod snapped_to;\nmod your_new_visitor;  // ← add here\n\npub fn visit_packages(ctx: Context) -> Context {\n    ctx.version_groups.iter().sorted_by(order_snapped_to_groups_last).for_each(|group| {\n        group.dependencies.values().for_each(|dependency| match dependency.variant {\n            VersionGroupVariant::Banned => banned::visit(dependency),\n            VersionGroupVariant::SameMinor => same_minor::visit(dependency),\n            // ... add your new variant arm here if a new VersionGroupVariant was added ...\n            VersionGroupVariant::SnappedTo => snapped_to::visit(dependency, &ctx),\n        });\n    });\n    ctx\n}\n```\n\n---\n\n## Step 6: Write tests\n\nTest files live alongside their visitor: `src/visit_packages/same_minor_test.rs`\nis declared inside `same_minor.rs` with:\n\n```rust\n#[cfg(test)]\n#[path = \"same_minor_test.rs\"]\nmod same_minor_test;\n```\n\nTests use `TestBuilder` and `expect(&ctx).to_have_instances(...)`. Every\n`ExpectedInstance` must specify:\n\n- `state` — the exact `InstanceState` expected\n- `dependency_name` — the dependency name (not the instance id)\n- `id` — `\"{dep} in {location} of {package}\"` format\n- `actual` — the on-disk specifier string\n- `expected` — `Some(\"...\")` for the expected/fix-target specifier, or `None`\n- `overridden` — `Some(\"...\")` when a semver group preference was overridden\n\n```rust\nuse {\n    crate::{\n        instance_state::{FixableInstance::*, InstanceState, UnfixableInstance::*, ValidInstance::*},\n        test::{\n            builder::TestBuilder,\n            expect::{expect, ExpectedInstance},\n        },\n    },\n    serde_json::json,\n};\n\n#[test]\nfn major_mismatch_marks_all_instances_unfixable() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![json!({\n            \"name\": \"my-project\",\n            \"version\": \"1.0.0\",\n            \"dependencies\": {\n                \"foo\": \"1.2.0\"\n            },\n            \"devDependencies\": {\n                \"foo\": \"2.1.0\"\n            }\n        })])\n        .with_version_groups(vec![json!({\n            \"dependencies\": [\"foo\"],\n            \"policy\": \"sameMinor\"\n        })])\n        .build_and_visit_packages();\n\n    expect(&ctx).to_have_instances(vec![\n        ExpectedInstance {\n            state: InstanceState::valid(IsLocalAndValid),\n            dependency_name: \"my-project\",\n            id: \"my-project in /version of my-project\",\n            actual: \"1.0.0\",\n            expected: Some(\"1.0.0\"),\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /dependencies of my-project\",\n            actual: \"1.2.0\",\n            expected: None,\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /devDependencies of my-project\",\n            actual: \"2.1.0\",\n            expected: None,\n            overridden: None,\n        },\n    ]);\n}\n```\n\nCover every leaf of the decision tree. For a visitor with semver group\ninteraction, the minimum test matrix is:\n\n| scenario                             | test name pattern                                          |\n| ------------------------------------ | ---------------------------------------------------------- |\n| no semver group, correct version     | `satisfies_..._and_has_no_semver_group`                    |\n| no semver group, wrong version       | `has_..._mismatch_and_no_semver_group`                     |\n| safe semver group, already matches   | `satisfies_..._and_matches_compatible_..._semver_group`    |\n| safe semver group, does not match    | `satisfies_..._but_mismatches_compatible_..._semver_group` |\n| unsafe semver group, already matches | `..._but_range_matches_incompatible_..._semver_group`      |\n| unsafe semver group, does not match  | `..._but_range_mismatches_incompatible_..._semver_group`   |\n\n---\n\n## Step 7: Verify commands need no changes\n\nCommands (`src/commands/*.rs`) operate on states generically:\n\n```rust\n// lint.rs, fix.rs etc. already handle your new variant automatically\ndependency.get_sorted_instances()\n    .filter(|instance| instance.is_invalid())  // or is_fixable(), is_unfixable() etc.\n    .for_each(|instance| { ... });\n```\n\nNew variants within existing categories (`Fixable`, `Unfixable`, etc.) are\npicked up automatically. You only need to touch command files if your new state\nrequires **bespoke fix logic** — for example, `IsBanned` requires removing the\ndependency entry rather than updating a version string.\n\n---\n\n## Step 8: Run the tests\n\n```bash\n# Run tests for a specific visitor\ncargo test same_minor\n\n# Run all tests\njust test\n```\n\n---\n\n## Checklist\n\n- [ ] Variant added to the correct enum in `src/instance_state.rs`\n- [ ] Doc comment uses `✓` `✘` `?` `!` bullets and describes every condition\n- [ ] Visitor uses `instance.mark_*()` methods — no direct state assignment\n- [ ] Per-dependency values computed once before the per-instance loop\n- [ ] Every instance is marked on every code path through the visitor\n- [ ] Tests cover every leaf of the decision tree\n- [ ] Tests use `expect(&ctx).to_have_instances(...)` with `ExpectedInstance`\n- [ ] No command changes needed (or bespoke fix logic added if required)\n\n---\n\n## Related files\n\n- `src/instance_state.rs` — all state variant definitions and annotations\n- `src/visit_packages.rs` — visitor dispatch\n- `src/visit_packages/*.rs` — visitor implementations\n- `src/visit_packages/*_test.rs` — test examples to copy patterns from\n- `src/commands/fix.rs` — example of how states drive fix behaviour\n- `.notes/reference/version-group-implementation-rules.md` — cross-cutting rules for all visitors\n",".claude/skills/document-code/SKILL.md":"---\nname: document-code\ndescription: Add and update the documentation website for Syncpack. Use when making user-facing changes to the codebase.\n---\n\n# Document Code\n\nGuide for adding and updating Syncpack's documentation website.\n\n## Quick Decision\n\nWhat are you documenting?\n\n| Adding/Updating...    | Location                                         |\n| --------------------- | ------------------------------------------------ |\n| Command docs          | `site/src/content/docs/command/[cmd].mdx`        |\n| Config property       | `site/src/content/docs/config/[prop].mdx`        |\n| Version group variant | `site/src/content/docs/version-groups/[var].mdx` |\n| Semver group variant  | `site/src/content/docs/semver-groups/[var].mdx`  |\n| Status code           | `site/src/content/docs/status/[code].mdx`        |\n| FAQ                   | `site/src/faq/[id].mdx`                          |\n| Shared option partial | `site/src/_partials/option/[opt].mdx`            |\n| Reusable component    | `site/src/components/[name].astro`               |\n\n## Workflow\n\n1. **Read source** — `cli.rs` for commands, `rcfile.rs` for config, `instance_state.rs` for status codes\n2. **Create/update MDX** — Match existing structure exactly\n3. **Add link identifier** — Update `globalReferenceLinks` in `astro.config.mjs`\n4. **Test locally** — `pnpm run dev` from `site/`\n5. **Verify links** — Check all `(IDENTIFIER)` links resolve\n\n## Source to Docs Mapping\n\n| Source                  | Documentation                                         |\n| ----------------------- | ----------------------------------------------------- |\n| `src/cli.rs` Subcommand | `site/src/content/docs/command/*.mdx`                 |\n| `src/rcfile.rs` Rcfile  | `site/src/content/docs/config/*.mdx`                  |\n| `src/instance_state.rs` | `site/src/content/docs/status/*.mdx`                  |\n| `src/specifier.rs`      | `site/src/content/docs/reference/specifier-types.mdx` |\n\n## Local Development\n\n```bash\ncd site\npnpm run dev          # Start at http://localhost:4321/syncpack\npnpm run build        # Verify build succeeds\n```\n\n## Site Overview\n\n- Source located at `site/src/`\n- Built with [Starlight](https://starlight.astro.build/) on top of [Astro](https://astro.build/)\n- Content at `site/src/content/docs/**/*.mdx`\n- Reusable partials at `site/src/_partials/**/*.mdx`\n- Reusable components at `site/src/components/*.astro`\n\n## Command Documentation\n\nEach command has a corresponding file at `site/src/content/docs/command/[command].mdx`.\n\n### Required Structure (in order)\n\n1. Description of the command's purpose from end user perspective\n2. `## Examples` — Single bash code block with commented examples\n3. `## Options` — Start with `<QuoteFilters />` callout, then `###` for each option alphabetically (`--help` last)\n\n### Shared Option Partials\n\nMany commands share options. Define reusable partials at `site/src/_partials/option/*.mdx`:\n\n```mdx\nimport ConfigOption from \"@partials/option/config.mdx\";\n\n<ConfigOption command=\"update\" />\n```\n\n## Config Documentation\n\nEach config property has a file at `site/src/content/docs/config/[property].mdx`.\n\n### Required Structure\n\n1. Description of the config's purpose from end user perspective\n2. `## Default Value` — Code block showing default (when applicable)\n3. `## Examples` — Code blocks with commented examples\n\n### Version Groups and Semver Groups\n\nEach variant has its own page:\n\n- `site/src/content/docs/version-groups/*.mdx`\n- `site/src/content/docs/semver-groups/*.mdx`\n\n#### Required Structure\n\n1. Description of the group's purpose from end user perspective\n2. `## Examples` — Code block per example, capture novel use cases\n3. `## Configuration` — `###` for each config property\n\n## Status Code Documentation\n\nEach status code has a file at `site/src/content/docs/status/[code].mdx`.\n\nReference page at `site/src/content/docs/reference/status-codes.mdx` explains the `InstanceState` enum and its nested enums (`ValidInstance`, `InvalidInstance`, `SuspectInstance`).\n\n## FAQ Documentation\n\n- Create FAQ files at `site/src/faq/[id].mdx`\n- Listed at `/syncpack/faq` via `site/src/pages/faq.astro`\n- Individual pages via `site/src/pages/faq/[id].astro`\n\n## Linking Between Pages\n\nUse named identifiers in markdown links:\n\n```mdx\nWhen using the [update](COMMAND_UPDATE) command\n```\n\nDefine identifiers in `globalReferenceLinks` function in `site/astro.config.mjs`.\n\n## Open Graph Images\n\nGenerated by `site/src/pages/og/[...path].png.ts` using [Satori](https://github.com/vercel/satori).\n\n## Troubleshooting\n\n| Problem                       | Fix                                           |\n| ----------------------------- | --------------------------------------------- |\n| Link not working              | Check identifier in `globalReferenceLinks`    |\n| Partial not rendering         | Verify import path uses `@partials/` alias    |\n| OG image broken               | Check `og/[...path].png.ts` handles new route |\n| Build fails                   | Run `pnpm run build` locally to see error     |\n| Page not appearing in sidebar | Check frontmatter and Starlight config        |\n\n## Checklist\n\nBefore submitting documentation:\n\n- [ ] Structure matches existing pages of same type\n- [ ] Link identifier added to `astro.config.mjs`\n- [ ] Local dev server renders correctly\n- [ ] Build succeeds (`pnpm run build`)\n- [ ] All internal links resolve\n",".claude/skills/fix-bug/SKILL.md":"---\nname: fix-bug\ndescription: Debug and fix bugs in Syncpack using scientific debugging methodology. Use when a test is failing, unexpected behaviour occurs, or investigating issues. Covers hypothesis-driven debugging and TDD-based fixes.\n---\n\n# Fix Bug\n\nGuide for debugging and fixing bugs in Syncpack.\n\n## Scientific Debugging Workflow\n\n1. **Observe** — Understand the symptom precisely\n2. **Hypothesise** — Form a theory about the root cause\n3. **Experiment** — Test the hypothesis with targeted changes\n4. **Validate** — Confirm the fix with tests\n5. **Verify** — Run full test suite\n\n## Step 1: Observe\n\nGather information before changing code:\n\n```bash\n# Run the failing test with output\ncargo test test_name -- --nocapture\n\n# Run with backtrace\nRUST_BACKTRACE=1 cargo test test_name\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- lint\n```\n\nQuestions to answer:\n\n- What is the exact error message?\n- What input triggers it?\n- What is the expected vs actual behaviour?\n\n## Step 2: Hypothesise\n\nCommon root causes by symptom:\n\n| Symptom              | Likely Cause                                            |\n| -------------------- | ------------------------------------------------------- |\n| State is `Unknown`   | `visit_packages` not called or instance skipped         |\n| Wrong state assigned | Validation logic order, earlier check overriding        |\n| Instance not found   | Package/dependency name mismatch, location format       |\n| Command not running  | Missing registration in cli.rs, main.rs, or commands.rs |\n| Panic/unwrap failure | Unexpected None or Err value                            |\n\n## Step 3: Experiment\n\nAdd targeted debug output:\n\n```rust\nuse log::debug;\n\ndebug!(\"Instance: {:#?}\", instance);\ndebug!(\"State before: {:?}\", instance.state.borrow());\n```\n\nSearch for related code:\n\n```bash\n# Find where state is assigned\nast-grep -p 'InstanceState::fixable' src/visit_packages/\n\n# Find similar patterns\nast-grep -p 'PATTERN' src/\n```\n\n## Step 4: Validate\n\nWrite a failing test FIRST (TDD):\n\n```rust\n#[test]\nfn reproduces_the_bug() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![/* minimal reproduction */])\n        .build_and_visit_packages();\n\n    // Assert expected behaviour (will fail initially)\n}\n```\n\nThen fix the code until the test passes.\n\n## Step 5: Verify\n\n```bash\njust test        # All tests pass\ncargo clippy     # No warnings\n```\n\n## Where to Look\n\n### By component\n\n| Issue with...    | Check...                                 |\n| ---------------- | ---------------------------------------- |\n| CLI parsing      | `src/cli.rs`                             |\n| Config loading   | `src/config.rs`, `src/rcfile.rs`         |\n| Package reading  | `src/packages.rs`, `src/package_json.rs` |\n| State assignment | `src/visit_packages/*.rs`                |\n| Command output   | `src/commands/*.rs`, `src/commands/ui/`  |\n| Version parsing  | `src/specifier/*.rs`                     |\n\n### By phase\n\n| Phase   | What happens                             | Files                                      |\n| ------- | ---------------------------------------- | ------------------------------------------ |\n| Create  | Read config, packages, collect instances | `context.rs`, `packages.rs`                |\n| Inspect | Assign InstanceState                     | `visit_packages.rs`, `visit_packages/*.rs` |\n| Run     | Process instances, output/write          | `commands/*.rs`                            |\n\n## Common Fixes\n\n### Instance state not being assigned\n\nCheck:\n\n1. Is the instance's version group variant handled in `visit_packages.rs`?\n2. Is there an early return skipping this instance?\n3. Is another validation assigning a state first?\n\n### Test assertion failing\n\nCheck:\n\n1. Is `build_and_visit_packages()` called (not just `build()`)?\n2. Is the location string format correct? (`\"{dep} in {location} of {package}\"`)\n3. Are there multiple instances of the same dependency?\n\n### Command not found\n\nVerify registration in all three places:\n\n1. `src/cli.rs` — `Subcommand` enum\n2. `src/main.rs` — match arm\n3. `src/commands.rs` — module declaration\n\n## Debugging Tips\n\n- **Isolate** — Create minimal reproduction\n- **Binary search** — Comment out code to narrow down\n- **Print early** — Add debug output before suspected area\n- **Check order** — Validation order matters (first match wins)\n- **Read tests** — Existing tests show expected behaviour\n\n## Anti-Patterns\n\n❌ Changing code without understanding the cause\n❌ Removing code just to make tests pass\n❌ Fixing symptoms instead of root cause\n❌ Skipping the test-first step\n",".claude/skills/front-loading/SKILL.md":"---\nname: front-loading\ndescription: Put the answer first, then context and details follow. Use when writing documentation, tutorials, error messages, or any communication where readers need to decide quickly if something is relevant to them.\n---\n\n# Front-Loading\n\nPut the answer first. Context follows.\n\n## What This Is\n\nFront-loading means leading with the most important information. Users decide in the first few words whether to keep reading.\n\nStructure your communication as:\n\n1. **The point** (first sentence)\n2. **Why it matters** (next 1–2 sentences)\n3. **Details and evidence** (rest of content)\n\nMost writers do the opposite: context first, then conclusion. Readers get lost in the journey before reaching the destination.\n\n## When to Use\n\n- Writing documentation or guides\n- Structuring arguments or explanations\n- Creating tutorial sections\n- Writing error messages\n- Composing email or feedback\n- Organizing any sequential information\n\nFront-load at every level: sentences, paragraphs, sections, documents.\n\n## How to Apply\n\n### 1. Lead with the Conclusion\n\nPut the answer first. Don't make readers wait.\n\n**Bad (context first):**\n\"There are many different ways to approach testing. Some teams test before writing code. Others test after. Some tests run quickly, others take longer. The most important thing is consistency. So you should write tests.\"\n\n**Good (conclusion first):**\n\"Write tests. Consistency matters more than approach.\"\n\nThen explain the trade-offs if needed.\n\n### 2. Answer the Reader's Question Immediately\n\nReaders have a question in mind. Answer it in the first sentence.\n\n**Bad (makes reader wait):**\n\"Syncpack is a CLI tool built in Rust. It uses a configuration file in JSON format. The configuration file specifies rules for how to synchronise dependencies. These rules can be customized per workspace...\"\n\nReader's question: \"How do I configure Syncpack?\" Buried in paragraph 3.\n\n**Good (answers immediately):**\n\"Configure Syncpack in `.syncpackrc.json` at your monorepo root. Specify version groups and rules for each.\"\n\nThen explain each option.\n\n### 3. Use Signposting\n\nTell readers what's coming before they read it.\n\n**Bad (no preview):**\n\"First, install Node 18. Then, verify the installation. Next, clone the repository. After that, install dependencies...\"\n\nReader: \"Where is this going?\"\n\n**Good (preview structure):**\n\"Three steps: Install Node, clone the repo, and run `npm install`.\"\n\nThen detail each step.\n\n### 4. When Explaining a Process, Show the End State First\n\nLet readers know what success looks like before they start.\n\n**Bad (journey without destination):**\n\"Follow these 12 steps to set up CI/CD. Step 1: Create a workflow file. Step 2: Add a trigger. Step 3: Define a job...\"\n\nReader: \"Why am I doing this?\"\n\n**Good (end state first):**\n\"Goal: Your PR automatically runs tests and fails if any break. Here's how to set it up in 12 steps.\"\n\nThen detail steps 1–12.\n\n### 5. Structure Paragraphs with Topic Sentence First\n\nEach paragraph's first sentence should be its main idea.\n\n**Bad (idea buried):**\n\"There are many different caching strategies. Some store values in memory. Others use disk. Still others use remote services. The strategy you choose depends on your use case. For most applications, in-memory caching is sufficient.\"\n\nReader has to parse the whole paragraph to find the point.\n\n**Good (topic first):**\n\"For most applications, in-memory caching is sufficient. Other strategies (disk, remote) exist for specific cases. Choose based on your performance and persistence needs.\"\n\n### 6. When Providing Options, Lead with the Recommendation\n\nDon't make readers compare three options to find the best one.\n\n**Bad (no guidance):**\n\"You can use approach A, B, or C. Each has trade-offs. A is simple but inflexible. B is flexible but complex. C is somewhere in between. Most projects use B or C.\"\n\nReader: \"So which should I use?\"\n\n**Good (recommendation first):**\n\"Use approach B (flexible but complex). It scales as your needs grow. Use approach A only if simplicity is critical. Approach C rarely adds value.\"\n\n## Examples\n\n### Documentation Section\n\n**Bad (context first):**\n\"The registry client handles communication with npm. Over time, performance became an issue. We realized the client was making unnecessary requests. After analysis, we found that caching solved the problem. So the registry client now caches responses.\"\n\n**Good (point first):**\n\"The registry client caches npm responses to improve performance. This reduced request overhead by 60%.\"\n\nThen explain the details.\n\n---\n\n### Error Message\n\n**Bad (generic context):**\n\"An operation has encountered an error condition. The system attempted to perform a task. This task requires certain prerequisites. One or more prerequisites were not met. Please verify your setup.\"\n\n**Good (specific answer):**\n\"Missing required field in config: 'versionGroups'. See `.syncpackrc.json` example.\"\n\n---\n\n### Tutorial Section\n\n**Bad (setup before goal):**\n\"Before you can run Syncpack, you need to set up your monorepo structure. This involves creating a root directory. Inside this directory, you need workspace packages. Each package should have a `package.json`. Once you have this structure, you can run Syncpack.\"\n\n**Good (goal first):**\n\"Goal: Run Syncpack to sync versions across your monorepo.\"\n\nThen list the prerequisites (monorepo structure, workspace packages).\n\n---\n\n### Workflow Documentation\n\n**Bad (steps without outcome):**\n\"Step 1: Write your implementation. Step 2: Write tests. Step 3: Run tests locally. Step 4: Push to GitHub. Step 5: Wait for CI. Step 6: Address failures. Step 7: Merge when green.\"\n\n**Good (outcome first):**\n\"Goal: Merge code only when tests pass and CI approves.\"\n\nThen list the steps to reach that goal.\n\n## Trade-offs\n\n### When NOT to front-load\n\n- **Narrative/storytelling:** Sometimes the journey is the point. Build suspense if appropriate.\n- **Teaching from first principles:** Some explanation is necessary before jumping to conclusions.\n- **Building trust:** Sometimes context _is_ the point (e.g., \"here's why we made this decision\").\n- **Sensitive feedback:** Sometimes leading with criticism is harmful. Lead with understanding instead.\n\nThe principle: **Match the reader's mental model.** If they need context to understand the conclusion, provide context first.\n\n### Common mistake\n\nOver-front-loading: \"Use this. End of story.\" without any reasoning alienates readers who need understanding.\n\nSolve by asking: \"Will the reader understand why this conclusion is right?\" If no, provide enough context. But lead with the conclusion, not the context.\n\n## Testing\n\nHow to know if you've front-loaded effectively:\n\n1. **First sentence test:** Can someone read only the first sentence and understand the main point?\n2. **Skim test:** Someone skims headings and bullets—do they get the gist?\n3. **Reader expectation test:** \"What happens next?\" readers predict after each section. Were they right?\n4. **Reduction test:** Remove everything after the first paragraph. Is the main point clear? (If yes, you front-loaded successfully.)\n5. **Fresh eyes test:** Have someone read the first paragraph and summarize. Did they get it right?\n\n## Application in Syncpack\n\nFront-loading improves:\n\n- **Skill descriptions:** Users know instantly if it's relevant\n- **Error messages:** Users understand what failed and how to fix it\n- **Documentation sections:** Readers find what they need faster\n- **Code comments:** Maintainers understand the \"why\" before the \"how\"\n- **Commit messages:** Reviewers know the intent before reading code\n- **Proposals or plans:** Stakeholders see the recommendation before the analysis\n\nEvery document you write is a race against the reader's attention span. Front-load to win that race.\n",".claude/skills/optimize-benchmarks/SKILL.md":"---\nname: optimize-benchmarks\ndescription: Iterative performance optimisation loop for syncpack-specifier. Runs benchmarks, identifies bottlenecks, applies optimisations, verifies tests pass and benchmarks improve, then repeats.\n---\n\n# Optimize Benchmarks\n\nIterative loop: benchmark, optimise, test, verify improvement, repeat.\n\n## Workflow\n\n### 1. Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --save-baseline before 2>&1 | tail -40\n```\n\nSave the output. Identify which variants are slowest.\n\n### 2. Identify Bottleneck\n\nFrom the full baseline, focus on the **slowest** benchmarks first.\n\nPriority order for specifier parsing:\n\n1. `Specifier::create` — the main parse function, called for every version string\n2. `parser::is_range` — checks 12 regexes sequentially\n3. `parser::is_exact` — checks 4 regexes sequentially\n4. `parser::is_complex_range` — splits, collects, iterates\n5. Individual regex matches in `regexes.rs`\n\n### 3. Apply ONE Optimisation\n\nMake a **single, focused change**. Do NOT bundle multiple optimisations — each must be independently measurable.\n\n### 4. Verify Tests Pass\n\n```bash\ncargo test -p syncpack-specifier 2>&1 | tail -5\n```\n\nIf tests fail, fix or revert. Never proceed with failing tests.\n\n### 5. Benchmark Against Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --baseline before 2>&1 | tail -40\n```\n\nLook for `[-XX.XXX% ...]` (improvement) or `[+XX.XXX% ...]` (regression).\n\n### 6. Evaluate\n\n- **Improved:** Report the gains. Update baseline: `cargo bench -p syncpack-specifier -- --save-baseline before`. Continue to step 2.\n- **No change:** Revert and try a different approach.\n- **Regressed:** Revert immediately.\n\n### 7. Repeat\n\nGo to step 2. Stop when:\n\n- User says stop\n- No bottlenecks remain\n- Gains are <1% across all benchmarks\n\n## Known Optimisation Opportunities\n\n### High Impact\n\n**Replace regex with char-based parsing in `parser.rs` / `regexes.rs`**\n\nMost regexes in `regexes.rs` match simple patterns like `^[0-9]+\\.[0-9]+\\.[0-9]+$` (exact semver). These can be replaced with byte/char iteration:\n\n```rust\n// Instead of regex EXACT: r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\"\nfn is_exact_version(s: &str) -> bool {\n  let mut dots = 0;\n  let bytes = s.as_bytes();\n  if bytes.is_empty() { return false; }\n  for &b in bytes {\n    match b {\n      b'0'..=b'9' => {},\n      b'.' => dots += 1,\n      _ => return false,\n    }\n  }\n  dots == 2\n}\n```\n\nRegex `is_match()` has overhead even for simple patterns: engine setup, capture group allocation. Char-based parsing for these patterns is 5-20x faster.\n\n**Reduce sequential regex attempts in `parser::is_range`**\n\n`is_range` tries 12 regexes. Instead, match on first char(s) to dispatch:\n\n```rust\nfn is_range(s: &str) -> bool {\n  match s.as_bytes().first() {\n    Some(b'^') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'~') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'>') => { /* check >= vs > then validate remainder */ },\n    Some(b'<') => { /* check <= vs < then validate remainder */ },\n    _ => false,\n  }\n}\n```\n\n**Consolidate related regex patterns**\n\nMany regexes are pairs: `EXACT` + `EXACT_TAG`, `CARET` + `CARET_TAG`, etc. Merge each pair into one function that handles both cases:\n\n```rust\nfn is_exact(s: &str) -> bool {\n  // Parse digits.digits.digits, then optionally -tag\n  let rest = parse_semver_triple(s)?;\n  rest.is_empty() || rest.starts_with('-')\n}\n```\n\n### Medium Impact\n\n**Replace `lazy_static` with `std::sync::OnceLock`**\n\n`lazy_static` uses an extra indirection layer. `OnceLock` (stable since Rust 1.80) is zero-cost after init:\n\n```rust\nuse std::sync::OnceLock;\n\nfn exact_regex() -> &'static Regex {\n  static RE: OnceLock<Regex> = OnceLock::new();\n  RE.get_or_init(|| Regex::new(r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\").unwrap())\n}\n```\n\nBut if regex is being replaced with char-based parsing, this becomes irrelevant.\n\n**Reorder checks in `Specifier::create` by frequency**\n\nIn a typical monorepo, most specifiers are `^x.y.z` (range) or `x.y.z` (exact). The current order already checks exact first, then range — good. But `is_exact` tries 4 regex patterns. A single fast char check can short-circuit:\n\n```rust\n// Fast path: first char is digit → likely exact or major or minor\n// Fast path: first char is ^ or ~ → likely range\n```\n\n**Avoid `String` allocation in `strip_semver_range`**\n\n`strip_semver_range` returns `&str` (already good), but callers like `Range::create` then `.to_string()` the result. Consider whether the allocation can be deferred.\n\n### Low Impact\n\n- Replace `HashMap` in caches with `FxHashMap` (faster hashing for short strings)\n- Use `SmallString` or stack-allocated strings for short specifiers\n- Pre-size cache `HashMap` with expected capacity\n\n## Architecture Notes\n\nKey files in `crates/syncpack-specifier/src/`:\n\n| File                         | Role                                                        |\n| ---------------------------- | ----------------------------------------------------------- |\n| `lib.rs`                     | `Specifier` enum, `create()` dispatch, caches               |\n| `parser.rs`                  | `is_exact()`, `is_range()`, etc. — classification functions |\n| `regexes.rs`                 | All `lazy_static` regex patterns                            |\n| `exact.rs`, `range.rs`, etc. | Variant constructors calling `node_semver`                  |\n| `semver_range.rs`            | `SemverRange` enum, `parse()`                               |\n\nThe hot path is: `Specifier::create()` → `parser::is_*()` → `regexes::*` → variant `::create()` → `node_semver` parsing.\n\nOptimising the `parser::is_*` layer gives the biggest wins because it runs for **every** specifier, and most of the time most checks return `false` (only one branch matches).\n\n## Rules\n\n- ONE change per iteration\n- Always verify tests pass before benchmarking\n- Always compare against baseline\n- Report numbers: `before → after (% change)`\n- Revert regressions immediately\n- Don't optimise what doesn't show up in benchmarks\n- Use **fast iteration** (`\"batch\"` filter) during the loop, **full suite** only at start and end\n",".claude/skills/review-docs/SKILL.md":"---\nname: review-docs\ndescription: Review and improve Syncpack documentation for clarity, completeness, and consistency. Use when enhancing docs or validating before publication.\n---\n\n# Review Docs\n\nReview and improve Syncpack documentation against project standards.\n\n## When to Use\n\n- Validating docs before publication\n- Improving existing documentation\n- Ensuring consistency across pages\n- User reports documentation is unclear\n\n## Related Skills\n\nUse with:\n\n- `document-code` — Reference for structure and locations\n- `signal-over-noise` — Apply to remove filler and obvious explanations\n- `front-loading` — Ensure actionable information comes first\n\n## Your Task\n\nGiven a documentation path, review content against standards. Suggest concrete improvements.\n\n## Quality Checklist\n\n- [ ] **Clarity**: First line immediately shows what it covers\n- [ ] **Examples**: Practical, show real use cases, cover major patterns\n- [ ] **Completeness**: All relevant aspects documented\n- [ ] **Consistency**: Formatting matches other docs\n- [ ] **Conciseness**: No fluff; every sentence earns space\n- [ ] **Accessibility**: Users find what they need without deep reading\n- [ ] **Reuse**: Reference shared partials when available\n\n## Review Process\n\n1. Read the documentation file\n2. Cross-reference structure with similar docs\n3. Identify information gaps where users might get stuck\n4. Check for shared partials at `site/src/_partials/`\n5. Apply signal-over-noise: Can you cut 20% of words without losing meaning?\n\n## Review Output\n\nProvide structured feedback:\n\n**Current State**\n\n- Assessment of documentation quality and coverage\n\n**Issues Found**\n\n- Specific problems with locations (section names or line references)\n- Prioritize by impact: clarity > completeness > polish\n\n**Recommendations**\n\n- Concrete improvements with examples\n- Rewrite short sections if needed\n- Flag missing content or examples\n- Suggest shared partials to reuse\n\n**Priority Actions**\n\n- Top 2-3 changes to make immediately\n\n## Reference\n\n**Docs:** `site/src/content/docs/`\n**Shared partials:** `site/src/_partials/`\n**Source of truth:** `src/` (Rust implementation)\n**Local testing:** `cd site && pnpm run dev` then verify at `http://localhost:4321/syncpack`\n",".claude/skills/search-code/SKILL.md":"---\nname: search-code\ndescription: Search for code patterns in Syncpack. Use when finding symbols, implementations, or understanding how code is used. Covers ast-grep for Rust and grep/rg for other cases.\n---\n\n# Search Code\n\nGuide for searching the Syncpack codebase effectively.\n\n## Golden Rule\n\n**Use `ast-grep` for Rust files** — it filters out comments, strings, and docs.\n\n## Quick Reference\n\n| Looking for...                    | Use         |\n| --------------------------------- | ----------- |\n| Rust symbols, patterns, structs   | `ast-grep`  |\n| Multiple file types               | `grep`/`rg` |\n| Content in comments intentionally | `grep`/`rg` |\n| Non-Rust files (JSON, MD, TOML)   | `grep`/`rg` |\n\n## ast-grep Examples\n\n```bash\n# Find struct definitions\nast-grep -p 'pub struct $NAME' src/\n\n# Find function calls\nast-grep -p 'Specifier::new' src/\n\n# Find enum variants\nast-grep -p 'pub enum $NAME' src/\n\n# Find impl blocks\nast-grep -p 'impl $TYPE' src/\n\n# Find specific method definitions\nast-grep -p 'pub fn run($$$) -> i32' src/\n\n# Find match expressions\nast-grep -p 'match $EXPR { $$$ }' src/\n```\n\n## Pattern Syntax\n\n| Pattern | Matches                                      |\n| ------- | -------------------------------------------- |\n| `$NAME` | Single identifier                            |\n| `$$$`   | Zero or more items (arguments, fields, etc.) |\n| `$_`    | Any single expression                        |\n\n## When to Use grep/rg\n\n```bash\n# Search across file types\nrg \"TODO\" --type rust --type md\n\n# Search in comments (ast-grep ignores these)\nrg \"FIXME\" src/\n\n# Search non-Rust files\nrg \"version\" Cargo.toml\n\n# Case-insensitive search\nrg -i \"error\" src/\n```\n\n## Common Searches\n\n### Find where a type is used\n\n```bash\nast-grep -p 'Context' src/\n```\n\n### Find all InstanceState assignments\n\n```bash\nast-grep -p 'InstanceState::valid' src/\nast-grep -p 'InstanceState::fixable' src/\nast-grep -p 'InstanceState::unfixable' src/\n```\n\n### Find test files\n\n```bash\nfind src -name '*_test.rs'\n```\n\n### Find command implementations\n\n```bash\nast-grep -p 'pub fn run($$$) -> i32' src/commands/\n```\n\n## Tips\n\n- Start broad, then narrow with more specific patterns\n- Use `ast-grep` output to find the file, then `read_file` to understand context\n- Chain searches: find the type, then find where it's constructed\n",".claude/skills/signal-over-noise/SKILL.md":"---\nname: signal-over-noise\ndescription: Maximize useful information per word by removing filler, obvious explanations, and hedging language. Use when writing documentation, error messages, code comments, or any communication where clarity and conciseness matter.\n---\n\n# Signal Over Noise\n\nMaximize useful information per word. Remove everything that doesn't earn its space.\n\n## What This Is\n\nSignal = information that moves the reader toward action or understanding.\nNoise = filler, explanations of obvious things, hedging language, unnecessary context.\n\nHigh signal-to-noise ratio means readers get what they need faster. Every word counts.\n\n## When to Use\n\n- Writing documentation, guides, skill descriptions\n- Creating error messages and feedback\n- Structuring arguments or explanations\n- Naming things (variables, functions, branches)\n- Any communication where clarity matters\n\n## How to Apply\n\n### 1. Remove Filler Words\n\n**High noise:**\n\n- \"basically\"\n- \"essentially\"\n- \"in order to\"\n- \"comprehensive\"\n- \"helpful\"\n- \"I think that\"\n- \"it could be argued that\"\n\n**Before:** \"Basically, you should essentially use this pattern in order to get comprehensive error handling.\"\n\n**After:** \"Use this pattern for error handling.\"\n\n### 2. Cut Explanations of Obvious Things\n\nDon't explain what readers already know. Assume competence.\n\n**Before:** \"Rust is a programming language. In Rust, you write code. This code runs on computers. When you compile Rust code, it produces a binary executable file.\"\n\n**After:** \"Compile Rust code to a binary with `cargo build --release`.\"\n\n### 3. Front-Load the Point\n\nPut the actionable information first. Context follows.\n\n**Before:** \"There are several reasons why you might want to consider testing your code. First, testing helps catch bugs. Second, it makes refactoring safer. Most importantly, tests serve as documentation. You should write tests.\"\n\n**After:** \"Write tests to catch bugs, enable safe refactoring, and document behavior.\"\n\n### 4. Use Active, Direct Language\n\n**High noise:** \"It is recommended that consideration be given to...\"\n\n**Signal:** \"Consider...\"\n\n**High noise:** \"The file was created by the user.\"\n\n**Signal:** \"Create the file.\"\n\n### 5. One Idea Per Sentence\n\nLong sentences hide the point. Break them.\n\n**Before:** \"When you're writing code that handles errors, which can happen in many places throughout your application, you should think about whether you want to handle them at the point where they occur or propagate them upward, and this decision affects how you structure your code.\"\n\n**After:** \"Handle errors where they occur or propagate them upward. This choice shapes your code structure. Decide early.\"\n\n### 6. Name Things Precisely\n\nNames are communication. Bad names add noise; good names clarify.\n\n**High noise:** `process_data()`, `handle_thing()`, `do_stuff()`\n\n**Signal:** `validate_user_input()`, `retry_failed_request()`, `parse_json_config()`\n\n### 7. Omit Context Readers Already Have\n\nDon't repeat what's obvious from surrounding text.\n\n**Before:** \"Here's an example of how to use the feature. In this example, we show you how to use it.\"\n\n**After:** \"Example:\"\n\n## Trade-offs\n\n### When NOT to apply aggressively\n\n- **Teaching beginners:** Some explanatory redundancy helps them build intuition\n- **Safety-critical systems:** Explicit redundancy catches misunderstanding\n- **Legal/compliance docs:** Specificity sometimes requires verbose phrasing\n- **User research notes:** Capture raw data, organize later\n\n### The balance\n\nSignal-over-noise isn't \"be terse.\" It's \"be useful.\" Sometimes that means:\n\n- Repeating a key phrase for emphasis\n- Providing context that prevents misreading\n- Showing your work so readers can verify reasoning\n- Acknowledging limitations explicitly\n\nRemove noise, keep signal. Don't remove useful context.\n\n## Examples\n\n### Code Documentation\n\n**Before:**\n\n```\nThis function is useful for validating email addresses.\nIt takes a string as input, which represents the email address\nthat you want to check. The function will return true if the\nemail is valid, or false if it is not valid. You might want to\nuse this function when you need to validate emails.\n```\n\n**After:**\n\n```\nValidate email addresses. Returns true if valid, false otherwise.\n```\n\n### Error Messages\n\n**Before:**\n\n```\nAn error has occurred in the system. The operation you attempted\ncould not be completed successfully. Please try again later or\ncontact support if the problem persists.\n```\n\n**After:**\n\n```\nFailed to fetch user data. Retry or contact support.\n```\n\n### Skill Description\n\n**Before:**\n\n```\nThis skill helps you think about how to communicate more effectively\nby using techniques that are designed to help you reduce unnecessary\ninformation and focus on the important parts of what you're trying\nto say so that your readers understand your message better and faster.\n```\n\n**After:**\n\n```\nMaximize useful information per word. Remove filler, obvious explanations, and hedging language. Every word should earn its space.\n```\n\n## Testing\n\nHow to know if you've applied this principle:\n\n1. **Read it aloud.** Does every sentence move you forward?\n2. **Ask \"why?\"** for each phrase. If the answer is \"readers already know this,\" cut it.\n3. **Count words.** Did you reduce the word count by 20–40%? Good sign.\n4. **Test comprehension.** Can a fresh reader understand it without re-reading?\n5. **Check names.** Are function/variable names predictable and specific?\n\n## Application in Syncpack\n\nThis principle strengthens all your other skills:\n\n- **write-code:** Function names, comments, commit messages all benefit\n- **document-code:** Docs communicate faster when noise is removed\n- **add-feature:** Feature descriptions and changelogs stay concise\n- **fix-bug:** Error messages become actionable, not explanatory\n- **search-code:** Clear naming makes code more discoverable\n\nApply signal-over-noise across the board. Your CLAUDE.md already mandates it—this skill formalizes how.\n",".claude/skills/testing-gaps/SKILL.md":"---\nname: testing-gaps\ndescription: Run coverage, inspect results, and identify missing test scenarios for a given source file. Use when analysing test coverage or finding untested branches.\n---\n\n# Testing Gaps\n\nFind untested branches and missing real-world test scenarios for a source file.\n\n## Workflow\n\n1. **Run text coverage** for the target file\n2. **Identify uncovered branches** from the output\n3. **Cross-reference** with test file to understand what's tested\n4. **Report** uncovered branches + missing real-world scenarios\n\n## Step 1: Run Coverage\n\nUse `cargo llvm-cov` with `--text` output (not `--html`) for parseable results.\n\n```bash\n# Full coverage, filtered to target file\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/TARGET_FILE\\.rs:/,/^$/p'\n```\n\nExample for `preferred_semver.rs`:\n\n```bash\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/preferred_semver\\.rs:/,/^$/p'\n```\n\n## Step 2: Read Coverage Output\n\nThe text format shows per-line execution counts:\n\n```\n  42|     80|        .and_then(|range| ...)     # Hit 80 times\n  74|      0|        Some(preferred)            # Never hit\n 321|      0|        instance.mark_conflict(...) # Never hit\n```\n\n- `count > 0` = covered\n- `count = 0` = uncovered branch\n- `^N` annotations on sub-expressions show partial coverage within a line\n\n### Extract only uncovered lines\n\n```bash\n... | grep -E '^\\s+\\d+\\|\\s+0\\|'\n```\n\n## Step 3: Classify Gaps\n\nFor each uncovered branch, determine:\n\n1. **What code path leads here?** — Trace the `if/else` chain backward\n2. **What input would trigger it?** — What package.json + config combination\n3. **Is it reachable?** — Some branches may be defensive/unreachable\n4. **Is it worth testing?** — Real-world scenario vs theoretical edge case\n\n### Categories\n\n| Category                         | Action            |\n| -------------------------------- | ----------------- |\n| Real-world scenario never tested | Write a test      |\n| Edge case in existing logic      | Write a test      |\n| Defensive branch (unreachable)   | Note but skip     |\n| Dead code                        | Consider removing |\n\n## Step 4: Identify Missing Scenarios\n\nBeyond line coverage, look for missing _combinations_:\n\n- Feature A tested, Feature B tested, A+B never tested together\n- Only tested with one package, never with multiple\n- Only tested with `highestSemver`, never `lowestSemver`\n- Only tested with one range type (`^`), never others (`>=`, `<=`)\n- Only tested without semver groups, never with\n\n## Step 5: Report\n\nStructure findings as:\n\n1. **Uncovered branches** — Specific lines, what triggers them, why untested\n2. **Missing real-world scenarios** — Combinations and interactions not covered\n\n## Running Tests After Adding Coverage\n\n```bash\njust test                              # All tests\ncargo test test_name -- --nocapture   # Specific test\njust coverage                         # Regenerate coverage report\n```\n",".claude/skills/write-code/SKILL.md":"---\nname: write-code\ndescription: Rust code style and conventions for Syncpack. Use when writing or modifying Rust code. Covers functional patterns, imports, naming, and quality standards.\n---\n\n# Write Code\n\nRust code conventions for Syncpack.\n\n## Style\n\n- **Functional style:** pipelines over loops\n- **Avoid `?` chains:** use `.and_then()`, `.map()`, `.or_else()`\n- **Descriptive names:** clarity over brevity\n- **Named placeholders:** `println!(\"{var}\")` not `println!(\"{}\", var)`\n- **British English:** \"behaviour\" not \"behavior\", \"organised\" not \"organized\"\n\n## Imports\n\nSingle `use` statement with grouped braces:\n\n```rust\nuse {\n  crate::{cli::Cli, config::Config},\n  log::{debug, error},\n  std::{process::exit, sync::Arc},\n};\n```\n\nRules:\n\n- **Never use `super::`** — always `crate::` for internal imports\n- Group: `crate::`, external crates, `std::`\n- Alphabetise within groups\n\n## File Organisation\n\n| Adding...   | Location                                                         |\n| ----------- | ---------------------------------------------------------------- |\n| New command | `src/commands/{name}.rs`                                         |\n| New test    | Sibling `_test.rs` file (e.g., `src/foo.rs` → `src/foo_test.rs`) |\n\n**NEVER** use `#[cfg(test)]` modules inside implementation files.\n\n## Quality\n\n- Functions <50 lines, commands 100-300 lines\n- Zero warnings (except during TDD red phase)\n- Run `just format` before committing\n\n## Comments\n\n### Forbidden\n\n- **`//!` module docs** — collect implementation history and rot\n- **Phase/F-N labels** — `// Phase v4-2`, `// F16:`, banner headers like `// ---------- Phase v4-3 RED ----------`\n- **History refs** — `Migrated from`, `carry-over`, `Mirrors v3`, `replaces the v3`, `previously inside X`, `reserved for future`\n- **Issue/PR refs** — `Reproduces issue #239`, `GitHub issue #206`. Plans rot; the code is the truth\n- **Banner separators** — `// ---------- Free functions: yaml ops ----------`. Use module structure or let the file speak for itself\n- **Test scenario labels** — don't write `// Windows-style backslashes` above a test input. Use a descriptive variable binding (`let windows_backslashes = [...]`) instead\n- **Name-restating field docs** — `/// A unique identifier for this instance`, `/// The dependency name`, `/// The instance id`. The field name already says it\n- **Step numbering inside functions** — `// 1. ...`, `// 2. ...`. Drop the numbers; keep the WHY if it's non-obvious\n\n### Allowed\n\n- **`@TODO` markers** — future plans worth tracking inline\n- **`///` doc comments** for non-obvious detail: `None`/`Err` semantics, side effects, invariants, distinctions between similar fields (e.g. `is_local_dependency` vs `is_local_instance`)\n- **`//` inline comments** when WHY is non-obvious: hidden constraints, ordering invariants (\"SnappedTo groups must be visited last\"), workarounds for surprising library behaviour, \"must not poison X\" cautions\n\n### Default\n\nNo comments. Add one only when the WHY is non-obvious to a fresh reader.\n\n## Patterns\n\n### Iterating Instances\n\n```rust\nctx.version_groups.iter().for_each(|group| {\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n        dependency.get_sorted_instances()\n            .filter(|instance| instance.is_invalid())\n            .for_each(|instance| { /* process */ });\n    });\n});\n```\n\n### Error Handling\n\nPrefer combinators over `?`:\n\n```rust\n// Good\npath.parent()\n    .and_then(|p| p.to_str())\n    .map(|s| s.to_string())\n    .unwrap_or_default()\n\n// Avoid\nlet parent = path.parent()?;\nlet str = parent.to_str()?;\nOk(str.to_string())\n```\n\n### State Mutation\n\n```rust\nlet mut state = instance.state.borrow_mut();\nif !state.is_invalid() {\n    *state = InstanceState::fixable(SomeVariant);\n}\n```\n",".claude/skills/write-tests/SKILL.md":"---\nname: write-tests\ndescription: Write tests for Syncpack using the TestBuilder pattern. Use when adding tests for commands, validation logic, or any new functionality. Covers TestBuilder API, assertion patterns, and common test scenarios.\n---\n\n# Write Tests\n\n## Golden rules\n\n- Use `TestBuilder` — never construct `Context` manually.\n- Assert with `expect(&ctx).to_have_instances(vec![...])` — never index `ctx.instances`.\n- Async-first: `#[tokio::test]` + `.run().await` is the canonical entry.\n\n## TDD\n\n1. Read 2-3 tests in the same `*_test.rs` and copy the pattern\n2. Write failing test → `just test` → confirm RED\n3. Ask before implementing\n4. Implement minimal code → GREEN → `just format`\n\n## Quick start\n\n```rust\nuse {\n  crate::{\n    instance::{FixableInstance::*, InstanceState, SuspectInstance::*, UnfixableInstance::*, ValidInstance::*},\n    test::{\n      builder::TestBuilder,\n      expect::{expect, ExpectedInstance},\n    },\n  },\n  serde_json::json,\n};\n\n#[tokio::test]\nasync fn pinned_version_replaces_anything_different() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"workspace:*\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"workspace:*\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Sub-module organisation\n\nGroup related scenarios under nested modules — see `pinned_test.rs`, `catalog_defs_test.rs`:\n\n```rust\nmod local {\n  use super::*;\n  #[tokio::test] async fn refuses_to_pin_local_version() { ... }\n}\n\nmod normal {\n  use super::*;\n  #[tokio::test] async fn an_already_pinned_version_is_valid() { ... }\n}\n\nmod registry_updates {\n  use super::*;\n  #[tokio::test] async fn def_marked_outdated_when_registry_has_newer_version() { ... }\n}\n```\n\n## File location\n\n| Test type                                    | Location                                    |\n| -------------------------------------------- | ------------------------------------------- |\n| Version-group behaviour (pin, ban, ranges …) | `src/version_group/<group>_test.rs`         |\n| Catalog discovery wiring                     | `src/version_group/{catalog,bun_catalog}_test.rs` |\n| Fix mutations                                | `src/commands/fix_test.rs`                  |\n| Format pass                                  | `src/visit_formatting/format_test.rs`       |\n| Other unit tests                             | Co-located: `src/foo.rs` ↔ `src/foo_test.rs` |\n\n## Builder methods\n\nSource of truth: `src/test/builder.rs`.\n\n| Method                                                  | Purpose                                                              |\n| ------------------------------------------------------- | -------------------------------------------------------------------- |\n| `.with_package(json!({...}))`                           | Add one package.json                                                 |\n| `.with_packages(vec![...])`                             | Add many                                                             |\n| `.with_version_group(json!({...}))`                     | Add one version group                                                |\n| `.with_version_groups(vec![...])`                       | Add many                                                             |\n| `.with_semver_group(json!({...}))`                      | Add a semver group                                                   |\n| `.with_config(json!({...}))`                            | Base config (e.g. `customTypes`, `dependencyGroups`)                 |\n| `.with_strict(bool)`                                    | Strict mode (Suspect → error)                                        |\n| `.with_subcommand(\"update\")`                            | Override subcommand (default: `lint`, or `update` if registry set)   |\n| `.with_pnpm_catalogs(yaml)`                             | Inject `pnpm-workspace.yaml`; implies pnpm PM                        |\n| `.with_bun_catalogs(json!({...}))`                      | Synthetic Bun root with `/catalog`, `/catalogs/{n}`; implies Bun PM  |\n| `.with_bun_workspaces_catalogs(json!({...}))`           | Same, nested under `/workspaces/`                                    |\n| `.with_{pnpm,bun,npm,yarn,unknown}_package_manager()`   | Force PM detection                                                   |\n| `.with_registry_updates(json!({\"react\":[...]}))`        | Mock npm registry; implies subcommand=`update`                       |\n| `.with_update_target(UpdateTarget::Minor)`              | Bound update target                                                  |\n| `.run().await` → `Context`                              | **Primary** end-to-end (full pipeline through disk + discovery)      |\n| `.build()`                                              | Sync, no visit — context-wiring tests                                |\n| `.build_and_visit_packages()`                           | Sync + visit_packages — fix tests, older suites                      |\n| `.build_and_visit_formatting()`                         | Sync + visit_formatting                                              |\n| `.build_with_registry_and_visit().await`                | Sync wiring + async registry mock + visit                            |\n\n## ExpectedInstance fields\n\n```rust\nExpectedInstance {\n  state: InstanceState::fixable(DiffersToPin),  // valid / fixable / unfixable / suspect\n  dependency_name: \"react\",                     // = `internal_name` (alias-aware)\n  id: \"react in /dependencies of package-a\",    // {dep} in {/path} of {package_or_yaml}\n  actual: \"17.0.0\",                             // raw specifier on disk\n  expected: Some(\"18.0.0\"),                     // None = ignore; Some(\"\") = remove\n  overridden: None,                             // semver-group override target, if any\n}\n```\n\n`id` location examples:\n\n- `/dependencies`, `/devDependencies`, `/peerDependencies`\n- `/version of package-a` (local version)\n- `/packageManager`, `/engines/node`\n- `/catalog of pnpm-workspace.yaml`, `/catalogs/<name> of pnpm-workspace.yaml`\n- `/customVersion`, `/custom/config/version` (via `customTypes`)\n\n## Patterns\n\n→ [patterns.md](patterns.md): banned, pinned, sameRange, pnpm catalogs, bun catalogs, semver ranges, registry updates.\n\n## Fix tests\n\n`src/commands/fix_test.rs` builds with `.build_and_visit_packages()` then runs `fix::run(ctx, &SilentReporter, &disk)`. `dry_run = true` is the default (set by `mock::config_from_mock`), so `is_dirty()` and post-fix contents stay observable. Set `ctx.config.cli.dry_run = false` only when asserting writes through a recording `MockDiskIo` (see `pnpm_fix_writes_yaml_to_disk`).\n\n## Common mistakes\n\n| Wrong                                            | Right                                                                                  |\n| ------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| `#[test] fn foo()` + `.run().await`              | `#[tokio::test] async fn foo()`                                                        |\n| `use crate::instance_state::*`                   | `use crate::instance::*`                                                               |\n| `\"pinned\": \"1.0.0\"`                              | `\"pinVersion\": \"1.0.0\"`                                                                |\n| `Context { ... }`                                | `TestBuilder::new()...`                                                                |\n| `ctx.instances[0]`                               | `expect(&ctx).to_have_instances(vec![...])`                                            |\n| `.build()` then check states                     | `.run().await` (or `.build_and_visit_packages()` for sync)                             |\n| Missing `SuspectInstance::*` import              | Import all 4: `FixableInstance::*, ValidInstance::*, SuspectInstance::*, UnfixableInstance::*` |\n\n## Running\n\n```bash\njust test                            # all\ncargo test pinned_test               # pattern match\ncargo test test_name -- --nocapture  # with stdout\n```\n\n## Reference tests\n\n- `src/version_group/banned_test.rs` — banned + custom types\n- `src/version_group/pinned_test.rs` — sub-modules, semver-group interaction\n- `src/version_group/same_range_test.rs` — range satisfaction\n- `src/version_group/catalog_test.rs` — pnpm catalogs\n- `src/version_group/bun_catalog_test.rs` — bun catalogs (sync `.build()`)\n- `src/version_group/preferred_semver_test.rs` — registry updates, update targets\n",".claude/skills/write-tests/patterns.md":"# Test patterns\n\nReal, trimmed examples for the common scenarios. Imports and `mod` wrapping omitted for brevity — see [SKILL.md](SKILL.md) for the full preamble.\n\n## Banned\n\n`src/version_group/banned_test.rs`. Removes a dep; refuses to ban a local-package version.\n\n```rust\n#[tokio::test]\nasync fn refuses_to_ban_local_version() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"version\": \"1.0.0\"}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"package-a\": \"1.1.0\"}}),\n    ])\n    .with_version_group(json!({\n      \"dependencies\": [\"package-a\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::suspect(InvalidLocalVersion),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::suspect(RefuseToBanLocal),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"package-a\",\n      id: \"package-a in /dependencies of package-b\",\n      actual: \"1.1.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Banned with `customTypes`\n\n```rust\n#[tokio::test]\nasync fn removes_instance_with_named_version_string_strategy() {\n  let ctx = TestBuilder::new()\n    .with_config(json!({\n      \"customTypes\": {\n        \"packageManager\": {\n          \"strategy\": \"name@version\",\n          \"path\": \"packageManager\"\n        }\n      }\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"packageManager\": \"pnpm@7.27.0\"\n    })])\n    .with_version_group(json!({\n      \"dependencies\": [\"pnpm\"],\n      \"dependencyTypes\": [\"packageManager\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"pnpm\",\n      id: \"pnpm in /packageManager of package-a\",\n      actual: \"7.27.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Pinned\n\n`src/version_group/pinned_test.rs`.\n\n```rust\n#[tokio::test]\nasync fn an_already_pinned_version_is_valid() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"1.2.0\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIdenticalToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"1.2.0\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Pin overrides a semver-group range\n\n```rust\n#[tokio::test]\nasync fn pin_overrides_semver_group_match() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"^1.0.0\"}\n    }))\n    .with_semver_group(json!({\"range\": \"^\", \"dependencies\": [\"foo\"]}))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.0.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::fixable(PinOverridesSemverRange),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"^1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: Some(\"^1.0.0\"),\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Same range\n\n`src/version_group/same_range_test.rs`. Use a `local`-ignored group as guard so the local instances don't pollute the assertion.\n\n```rust\n#[tokio::test]\nasync fn ranges_satisfy_each_other() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"<=2.0.0\"}}),\n    ])\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-a\",\n      actual: \">=1.0.0\",\n      expected: Some(\">=1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-b\",\n      actual: \"<=2.0.0\",\n      expected: Some(\"<=2.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nFor mismatching ranges the consumer instances become `InstanceState::unfixable(SameRangeMismatch)`.\n\n## Pnpm catalogs\n\n`src/version_group/catalog_test.rs`. `with_pnpm_catalogs` injects `pnpm-workspace.yaml`. Catalog definitions are themselves instances under `/catalog of pnpm-workspace.yaml`.\n\n```rust\n#[tokio::test]\nasync fn catalog_group_definition_valid() {\n  let yaml = \"catalog:\\n  react: ^18.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_version_group(json!({\n      \"label\": \"enforce catalog\",\n      \"dependencies\": [\"react\"],\n      \"policy\": \"catalog\",\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalogDefinition),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^18.0.0\",\n      expected: Some(\"^18.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nNamed catalog: yaml `catalogs:\\n  react18:\\n    react: ^18.0.0\\n`, consumer `\"react\": \"catalog:react18\"`, def id `react in /catalogs/react18 of pnpm-workspace.yaml`.\n\n## Bun catalogs\n\n`src/version_group/bun_catalog_test.rs`. Two shapes:\n\n- `with_bun_catalogs(json!({...}))` puts `/catalog`, `/catalogs/{n}` at the synthetic Bun root.\n- `with_bun_workspaces_catalogs(json!({...}))` nests them under `/workspaces/`.\n\nThese tests often use sync `.build()` — they're checking discovery wiring, not the visit pipeline:\n\n```rust\n#[test]\nfn bun_top_level_catalog_definition_emits_instance() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\"name\": \"pkg-a\", \"version\": \"0.0.0\"})])\n    .build();\n  let react = ctx\n    .instances\n    .iter()\n    .find(|i| i.descriptor.name == \"react\")\n    .expect(\"expected a react bun catalog instance\");\n  assert!(react.is_catalog_instance());\n  assert_eq!(react.descriptor.specifier.get_raw(), \"^18.0.0\");\n}\n```\n\nFull validation pass (consumer + def states):\n\n```rust\n#[test]\nfn bun_catalog_definition_with_consumer_valid() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"}\n    })])\n    .build_and_visit_packages();\n  // assert states via expect(&ctx).to_have_instances(...) as elsewhere\n}\n```\n\n## Semver groups\n\nA semver group rewrites the *expected* range; mismatches are `SemverRangeMismatch` (fixable). Combine with version groups to test interaction:\n\n```rust\n#[tokio::test]\nasync fn semver_group_targets_one_package_only() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"^1.2.3\"}}),\n    ])\n    .with_semver_group(json!({\"packages\": [\"package-b\"], \"range\": \"^\"}))\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  // package-b's `^1.2.3` matches the `^` semver group → no mismatch\n  // assert SatisfiesSameRangeGroup on both consumer instances\n}\n```\n\n## Registry updates\n\n`with_registry_updates` mocks the npm registry and implies subcommand `update`. Optional `with_update_target` bounds it.\n\n```rust\n#[tokio::test]\nasync fn pnpm_catalog_def_marked_outdated_when_registry_has_newer_version() {\n  let yaml = \"catalog:\\n  react: ^17.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_registry_updates(json!({\"react\": [\"17.0.0\", \"18.5.0\"]}))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToNpmRegistry),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^17.0.0\",\n      expected: Some(\"^18.5.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nBound the update target:\n\n```rust\nlet ctx = TestBuilder::new()\n  .with_package(json!({\"name\": \"package-a\", \"dependencies\": {\"wat\": \"1.2.3\"}}))\n  .with_update_target(UpdateTarget::Minor)\n  .with_registry_updates(json!({\"wat\": [\"1.2.3\", \"1.3.4\", \"2.0.0\"]}))\n  .run()\n  .await;\n// wat → 1.3.4 (minor cap), not 2.0.0\n```\n\n`UpdateTarget` import: `use crate::cli::UpdateTarget;`.\n"},"files":{".claude/rules/syncpack.md":"# Syncpack\n\nRust CLI tool for synchronising npm dependency versions across JavaScript monorepos.\n\n## Getting Started\n\n1. Run `just` to see available commands\n2. Read `.notes/` for architecture and mental model\n3. Check `skills/` for task-specific guides:\n\n## Communication Style\n\n- Signal over noise\n- Extremely concise. Sacrifice grammar for brevity\n- Action-oriented: what to DO\n- Grounded in facts: cite code/docs\n- BANNED WORDS: basically, essentially, in order to, comprehensive\n- Always use AskUserQuestion when asking questions\n\n## Pre-Implementation Checklist\n\nBefore large changes, answer YES to all:\n\n- Have I identified ALL decision points?\n- Have I listed trade-offs for each approach?\n- Have I asked user which strategy to use?\n- Am I NOT making architectural assumptions?\n- Am I NOT creating files user didn't request?\n\n## Hard Rules: Ask First\n\nMUST ask when:\n\n- User intent unclear or multiple valid approaches exist\n- Breaking changes or core architecture modifications\n- Creating ANY new files/modules not explicitly requested\n- Large refactors (identify ALL decision points first)\n\nMUST NOT:\n\n- Use banned words\n- Assume user wants plan or summary documentation\n- Refactor without asking\n- Make architectural decisions unilaterally\n\n## Proceed Without Asking\n\n- Pattern clearly exists in codebase\n- Following established convention\n- Non-breaking changes\n- Adding tests, fixing obvious bugs\n\n## Documentation Rules: MANDATORY\n\n**NEVER create new documentation files unless explicitly requested.**\n\nExamples of BANNED actions:\n\n- Creating \"status\" docs, \"validation\" docs, \"current-status\" files\n- Creating \"summary\" docs, \"guide\" docs, \"checklist\" files\n- Splitting existing docs into multiple files\n- Creating \"handoff\" docs, \"next steps\" docs\n\n**ONLY update existing docs:**\n\n- Update the original document in place\n- Add sections/checkboxes to existing files\n- Mark tasks complete where they already exist\n\n**If user has ONE plan file, keep ONE plan file.**\n\nDo NOT proliferate documentation. Update existing files only.\n",".claude/skills/add-feature/SKILL.md":"---\nname: add-feature\ndescription: Add new features to Syncpack including commands and validation logic. Use when implementing new CLI commands, adding InstanceState variants, or extending version group behaviour.\n---\n\n# Add Feature\n\nGuide for adding new functionality to Syncpack.\n\n## Quick Decision\n\nWhat are you adding?\n\n- **New CLI command** (lint, fix, format, etc.) → See [adding-command.md](adding-command.md)\n- **New validation logic** (InstanceState variant) → See [adding-instance-state.md](adding-instance-state.md)\n- **Both** → Start with instance state, then add command\n\n## Prerequisites\n\nBefore adding any feature:\n\n1. Understand the 3-phase pattern: Create Context → Inspect Context → Run Command\n2. Know which phase your feature affects\n3. Check if similar code exists (use `ast-grep -p 'PATTERN' src/`)\n\n## Feature Types\n\n### Commands\n\nCommands are user-facing operations (`syncpack lint`, `syncpack fix`, etc.).\n\n**When to create a new command:**\n\n- New user-facing operation that doesn't fit existing commands\n- Different output format or behaviour needed\n\n**Registration points (all three required):**\n\n1. `src/cli.rs` - Add to `Subcommand` enum\n2. `src/main.rs` - Add match arm for dispatch\n3. `src/commands/*.rs` - Implement `pub fn run(ctx: Context) -> i32`\n\n→ Full guide: [adding-command.md](adding-command.md)\n\n### Instance States\n\nInstanceState variants describe validation results (valid, fixable, unfixable, suspect).\n\n**When to add a new state:**\n\n- New validation rule needed\n- New type of error/warning to report\n- New auto-fix capability\n\n**Location:** `src/instance_state.rs` + `src/visit_packages/*.rs`\n\n→ Full guide: [adding-instance-state.md](adding-instance-state.md)\n\n## Common Workflow\n\n1. Write failing test using TestBuilder\n2. Implement minimal code to pass\n3. Run `cargo clippy` and fix warnings\n4. Refactor if needed\n5. Update documentation\n\n## Checklist\n\nBefore submitting:\n\n- [ ] Tests pass (`just test`)\n- [ ] Zero clippy warnings\n- [ ] Follows existing patterns\n- [ ] Registered in all required places (for commands)\n- [ ] Uses TDD approach\n",".claude/skills/add-feature/adding-command.md":"# Example: Adding a New Command\n\n<scenario>\nThis guide walks through adding a new `syncpack check` command that lists outdated dependencies without modifying files.\n</scenario>\n\n<step number=\"1\">\n\n## Step 1: Decide on the Visitor\n\n**Question:** Does this command deal with dependency versions or package.json formatting?\n\n**Answer:** Dependency versions → Use `visit_packages`\n\n</step>\n\n<step number=\"2\">\n\n## Step 2: Add Command Enum Variant\n\n**File:** `src/cli.rs`\n\n```rust\npub enum Subcommand {\n  Lint,\n  Fix,\n  Format,\n  Update,\n  List,\n  Json,\n  Check,  // ← Add this\n}\n```\n\n</step>\n\n<step number=\"3\">\n\n## Step 3: Create Command Implementation\n\n**File:** `src/commands/check.rs`\n\n<implementation>\n\n```rust\nuse crate::{commands::ui, context::Context};\n\n/// List outdated dependencies without modifying files\npub fn run(ctx: Context) -> i32 {\n  let mut has_outdated = false;\n\n  ctx.version_groups.iter().for_each(|group| {\n    let mut has_printed_group = false;\n\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n      let mut has_printed_dependency = false;\n\n      dependency\n        .get_sorted_instances()\n        .filter(|instance| instance.is_outdated())\n        .for_each(|instance| {\n          // Lazy print headers only when we find outdated instances\n          if !has_printed_group {\n            ui::group::print_header(&ctx, group);\n            has_printed_group = true;\n          }\n          if !has_printed_dependency {\n            ui::dependency::print(&ctx, dependency, &group.variant);\n            has_printed_dependency = true;\n          }\n\n          // Print the outdated instance\n          if ctx.config.cli.show_instances {\n            ui::instance::print(&ctx, instance, &group.variant);\n          }\n\n          has_outdated = true;\n        });\n    });\n  });\n\n  if has_outdated {\n    1  // Exit with error code\n  } else {\n    ui::util::print_no_issues_found();\n    0  // Exit successfully\n  }\n}\n```\n\n</implementation>\n\n<pattern_notes>\n\n**Pattern notes:**\n\n- Take ownership of `Context`\n- Return `i32` exit code (0 = success, 1 = failure)\n- Use lazy printing (only print headers when needed)\n- Iterate: version groups → dependencies → instances\n- Filter instances by state (`.filter(|instance| instance.is_outdated())`)\n- Use existing UI utilities for consistent output\n\n</pattern_notes>\n\n</step>\n\n<step number=\"4\">\n\n## Step 4: Register Command in Module\n\n**File:** `src/commands.rs`\n\n```rust\npub mod fix;\npub mod format;\npub mod json;\npub mod lint;\npub mod list;\npub mod update;\npub mod check;  // ← Add this\n```\n\n</step>\n\n<step number=\"5\">\n\n## Step 5: Add to Main Dispatch\n\n**File:** `src/main.rs`\n\n```rust\nuse {\n  crate::{\n    commands::{check, fix, format, json, lint, list, update},  // ← Add check\n    // ... other imports\n  },\n};\n\n// ... in main() function:\n\nlet _exit_code = match ctx.config.cli.subcommand {\n  Subcommand::Fix => {\n    let ctx = visit_packages(ctx);\n    fix::run(ctx)\n  }\n  Subcommand::Format => {\n    let ctx = visit_formatting(ctx);\n    format::run(ctx)\n  }\n  Subcommand::Lint => {\n    let ctx = visit_packages(ctx);\n    lint::run(ctx)\n  }\n  Subcommand::Update => {\n    let mut ctx = ctx;\n    ctx.fetch_all_updates().await;\n    let ctx = visit_packages(ctx);\n    update::run(ctx)\n  }\n  Subcommand::List => {\n    let ctx = visit_packages(ctx);\n    list::run(ctx)\n  }\n  Subcommand::Json => {\n    let ctx = visit_packages(ctx);\n    json::run(ctx)\n  }\n  Subcommand::Check => {  // ← Add this\n    let ctx = visit_packages(ctx);\n    check::run(ctx)\n  }\n};\n```\n\n<key_points>\n\n**Key points:**\n\n- Must call `visit_packages(ctx)` before command (to assign InstanceStates)\n- Pass the returned Context to your command's run function\n- The command consumes Context and returns exit code\n\n</key_points>\n\n</step>\n\n<step number=\"6\">\n\n## Step 6: Add CLI Help Text (Optional)\n\n**File:** `src/cli.rs`\n\n```rust\n#[derive(Debug, clap::Parser)]\n#[command(name = \"syncpack\")]\n#[command(about = \"Manage multiple package.json files\")]\npub struct Cli {\n  #[command(subcommand)]\n  pub subcommand: Subcommand,\n  // ... other fields\n}\n\n#[derive(Debug, clap::Subcommand)]\npub enum Subcommand {\n  /// Find and report outdated dependencies\n  #[command(name = \"check\")]\n  Check,\n  // ... other variants\n}\n```\n\n</step>\n\n<step number=\"7\">\n\n## Step 7: Write Tests\n\n**File:** `src/commands/check_test.rs` (or add to existing test file)\n\n<test_examples>\n\n```rust\nuse {\n  crate::{\n    commands::check,\n    instance_state::{FixableInstance::*, InstanceState, ValidInstance::*},\n    test::{builder::TestBuilder, expect::ExpectedInstance},\n  },\n  serde_json::json,\n};\n\n#[test]\nfn reports_outdated_dependencies() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"17.0.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"17.0.0\", \"17.0.1\", \"18.0.0\", \"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  // Verify the instance is marked as outdated\n  let react_instance = ctx.instances.iter()\n    .find(|i| i.dependency.name == \"react\")\n    .expect(\"Should find react instance\");\n\n  assert!(react_instance.is_outdated());\n\n  // Run the command (in real test, you'd capture stdout)\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 1, \"Should exit with error when outdated found\");\n}\n\n#[test]\nfn exits_successfully_when_up_to_date() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"18.2.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 0, \"Should exit successfully when up to date\");\n}\n```\n\n</test_examples>\n\n</step>\n\n<step number=\"8\">\n\n## Step 8: Test Locally\n\n```bash\n# Build and run\ncargo run -- check --help\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- check\n\n# Run tests\ncargo test check\njust test\n```\n\n</step>\n\n<step number=\"9\">\n\n## Step 9: Verify All Places Updated\n\n<checklist>\n\n- [x] Added to `Subcommand` enum in `src/cli.rs`\n- [x] Created `src/commands/check.rs` with `pub fn run(ctx: Context) -> i32`\n- [x] Registered in `src/commands.rs` module\n- [x] Added match arm in `src/main.rs`\n- [x] Called appropriate visitor (`visit_packages`)\n- [x] Written tests\n- [x] Tested locally\n\n</checklist>\n\n</step>\n\n<variations>\n\n## Common Variations\n\n<variation type=\"file_modification\">\n\n### Command that modifies files\n\n```rust\npub fn run(mut ctx: Context) -> i32 {\n  // ... iterate and modify instances\n\n  // Write changes to disk\n  ctx.packages.write_all();\n\n  0\n}\n```\n\n</variation>\n\n<variation type=\"registry\">\n\n### Command that needs registry data\n\nIn `main.rs`:\n\n```rust\nSubcommand::MyCommand => {\n  let mut ctx = ctx;\n  ctx.fetch_all_updates().await;  // Fetch from npm\n  let ctx = visit_packages(ctx);\n  my_command::run(ctx)\n}\n```\n\n</variation>\n\n<variation type=\"cli_options\">\n\n### Command with specific CLI options\n\n```rust\npub enum Subcommand {\n  #[command(name = \"check\")]\n  Check {\n    /// Only check production dependencies\n    #[arg(long)]\n    prod_only: bool,\n  },\n}\n```\n\nAccess in command:\n\n```rust\nif let Subcommand::Check { prod_only } = &ctx.config.cli.subcommand {\n  if *prod_only {\n    // Filter logic\n  }\n}\n```\n\n</variation>\n\n</variations>\n\n<troubleshooting>\n\n## Troubleshooting\n\n<problem>\n**Problem:** Command not found\n→ Check you added it to all three places: enum, main.rs match, commands module\n</problem>\n\n<problem>\n**Problem:** Instances have `Unknown` state\n→ You forgot to call `visit_packages(ctx)` before your command\n</problem>\n\n<problem>\n**Problem:** Compilation error about moved value\n→ Make sure your command signature is `pub fn run(ctx: Context) -> i32`\n</problem>\n\n<problem>\n**Problem:** Changes not persisting\n→ You need to call `ctx.packages.write_all()` in commands that modify files\n</problem>\n\n</troubleshooting>\n\n<related_examples>\n\n## Related Examples\n\n- See `src/commands/lint.rs` for read-only command\n- See `src/commands/fix.rs` for file-modifying command\n- See `src/commands/update.rs` for command using registry client\n\n</related_examples>\n",".claude/skills/add-feature/adding-instance-state.md":"# Adding a New InstanceState Variant\n\n<scenario>\nThis guide walks through adding new `InstanceState` variants. The example used\nthroughout is `SameMinorHasMajorMismatch`, a real variant added during the\nsameMinor improvement work. It is an `Unfixable` state assigned when instances\nin a `sameMinor` version group have different MAJOR versions.\n</scenario>\n\n---\n\n## Step 1: Choose the right category\n\n```\nInstanceState\n├── Unknown              — never assign this; it is the initial pre-inspection state\n├── Valid(ValidInstance) — instance follows all rules correctly\n├── Invalid(InvalidInstance)\n│   ├── Fixable(FixableInstance)                  — syncpack knows the correct value and can auto-fix\n│   ├── Unfixable(UnfixableInstance)              — ambiguous; needs human decision\n│   └── Conflict(SemverGroupAndVersionConflict)   — two active rules are simultaneously unsatisfiable\n└── Suspect(SuspectInstance)                      — user misconfiguration\n```\n\nAsk:\n\n- Do we know the correct value? → `Fixable`\n- Is the situation ambiguous, multiple valid options? → `Unfixable`\n- Are two rules active and irreconcilable? → `Conflict`\n- Has the user misconfigured something? → `Suspect`\n- Is everything fine? → `Valid`\n\nFor `SameMinorHasMajorMismatch`: crossing a major version boundary is unsafe\nand we cannot pick a winner without asking the user → `Unfixable`.\n\n---\n\n## Step 2: Add the variant to `src/instance_state.rs`\n\nFind the appropriate enum and add the variant with a multi-line doc comment.\nEvery condition relevant to the state must be listed. Use these symbols:\n\n- `✓` — condition is true / requirement is met\n- `✘` — condition is false / requirement is violated\n- `?` — ambiguous / unknown / why we cannot auto-fix\n- `!` — notable side effect or policy decision\n\n```rust\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum UnfixableInstance {\n  // ... existing variants ...\n\n  /// - ✘ Instance is in a sameMinor version group\n  /// - ✘ One or more other instances have a different MAJOR version\n  /// - ? Crossing a major version boundary is unsafe\n  /// - ? We cannot know which MAJOR the user wants and have to ask them\n  SameMinorHasMajorMismatch,\n}\n```\n\nThe doc comment is the source of truth for when this state is assigned. Be\nprecise — every bullet should be independently verifiable from the visitor code.\n\n---\n\n## Step 3: Check whether any `impl` blocks need updating\n\nMost additions require no changes to `impl InstanceState`. The severity, the\n`is_fixable()` / `is_unfixable()` helpers, and the `get_name()` / `get_status_type()`\nmethods all work by matching on the outer category (`Valid`, `Invalid`,\n`Suspect`), not on individual variants.\n\nThe only time you need to touch `impl InstanceState` is if you are adding a\nvariant that needs a **custom severity** (rare) or a new top-level predicate\n(e.g. `is_banned()`). For ordinary additions, no changes are needed.\n\n---\n\n## Step 4: Write the detection logic in the visitor\n\nVisitor files live in `src/visit_packages/`. Each visitor receives a\n`&Dependency` and must call one of the mark methods on every instance:\n\n```rust\ninstance.mark_valid(ValidInstance::SatisfiesSameMinorGroup, specifier);\ninstance.mark_fixable(FixableInstance::DiffersToPin, expected_specifier);\ninstance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\ninstance.mark_conflict(SemverGroupAndVersionConflict::MatchConflictsWithLocal);\ninstance.mark_suspect(SuspectInstance::InvalidLocalVersion);\n```\n\nNever assign to `instance.state` directly — always use these methods.\n\nThe specifier argument to `mark_valid` and `mark_fixable` is the **expected**\nspecifier — what the instance should be set to. Read the actual specifier from\n`instance.descriptor.specifier`.\n\nExample — marking all instances when a major mismatch is detected (computed\nonce before the per-instance loop):\n\n```rust\nlet has_major_mismatch = dependency\n    .instances\n    .iter()\n    .any(|i| !i.already_has_same_major_as_all(&dependency.instances));\n\ndependency.instances.iter().for_each(|instance| {\n    let actual_specifier = &instance.descriptor.specifier;\n\n    if has_major_mismatch {\n        instance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\n        return;\n    }\n\n    // ... rest of visitor logic ...\n});\n```\n\nKey rules:\n\n- **Every instance must be marked** before the visitor returns. No instance may\n  remain in `InstanceState::Unknown` after `visit_packages()` completes.\n- **Compute per-dependency values once** before the loop (e.g. highest\n  specifier, major-mismatch boolean) to avoid O(n²) re-checking.\n- **Do not read `instance.state` inside the visitor** to decide what to do —\n  states are write-once during inspection.\n\n---\n\n## Step 5: Integrate into `visit_packages.rs` (only for new visitors)\n\nIf you are adding a variant to an **existing** visitor, skip this step.\n\nIf you are adding a **new visitor module**, register it in\n`src/visit_packages.rs`:\n\n```rust\nmod banned;\nmod ignored;\nmod indent;\nmod pinned;\nmod preferred_semver;\nmod same_minor;   // ← example of an existing entry\nmod same_range;\nmod snapped_to;\nmod your_new_visitor;  // ← add here\n\npub fn visit_packages(ctx: Context) -> Context {\n    ctx.version_groups.iter().sorted_by(order_snapped_to_groups_last).for_each(|group| {\n        group.dependencies.values().for_each(|dependency| match dependency.variant {\n            VersionGroupVariant::Banned => banned::visit(dependency),\n            VersionGroupVariant::SameMinor => same_minor::visit(dependency),\n            // ... add your new variant arm here if a new VersionGroupVariant was added ...\n            VersionGroupVariant::SnappedTo => snapped_to::visit(dependency, &ctx),\n        });\n    });\n    ctx\n}\n```\n\n---\n\n## Step 6: Write tests\n\nTest files live alongside their visitor: `src/visit_packages/same_minor_test.rs`\nis declared inside `same_minor.rs` with:\n\n```rust\n#[cfg(test)]\n#[path = \"same_minor_test.rs\"]\nmod same_minor_test;\n```\n\nTests use `TestBuilder` and `expect(&ctx).to_have_instances(...)`. Every\n`ExpectedInstance` must specify:\n\n- `state` — the exact `InstanceState` expected\n- `dependency_name` — the dependency name (not the instance id)\n- `id` — `\"{dep} in {location} of {package}\"` format\n- `actual` — the on-disk specifier string\n- `expected` — `Some(\"...\")` for the expected/fix-target specifier, or `None`\n- `overridden` — `Some(\"...\")` when a semver group preference was overridden\n\n```rust\nuse {\n    crate::{\n        instance_state::{FixableInstance::*, InstanceState, UnfixableInstance::*, ValidInstance::*},\n        test::{\n            builder::TestBuilder,\n            expect::{expect, ExpectedInstance},\n        },\n    },\n    serde_json::json,\n};\n\n#[test]\nfn major_mismatch_marks_all_instances_unfixable() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![json!({\n            \"name\": \"my-project\",\n            \"version\": \"1.0.0\",\n            \"dependencies\": {\n                \"foo\": \"1.2.0\"\n            },\n            \"devDependencies\": {\n                \"foo\": \"2.1.0\"\n            }\n        })])\n        .with_version_groups(vec![json!({\n            \"dependencies\": [\"foo\"],\n            \"policy\": \"sameMinor\"\n        })])\n        .build_and_visit_packages();\n\n    expect(&ctx).to_have_instances(vec![\n        ExpectedInstance {\n            state: InstanceState::valid(IsLocalAndValid),\n            dependency_name: \"my-project\",\n            id: \"my-project in /version of my-project\",\n            actual: \"1.0.0\",\n            expected: Some(\"1.0.0\"),\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /dependencies of my-project\",\n            actual: \"1.2.0\",\n            expected: None,\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /devDependencies of my-project\",\n            actual: \"2.1.0\",\n            expected: None,\n            overridden: None,\n        },\n    ]);\n}\n```\n\nCover every leaf of the decision tree. For a visitor with semver group\ninteraction, the minimum test matrix is:\n\n| scenario                             | test name pattern                                          |\n| ------------------------------------ | ---------------------------------------------------------- |\n| no semver group, correct version     | `satisfies_..._and_has_no_semver_group`                    |\n| no semver group, wrong version       | `has_..._mismatch_and_no_semver_group`                     |\n| safe semver group, already matches   | `satisfies_..._and_matches_compatible_..._semver_group`    |\n| safe semver group, does not match    | `satisfies_..._but_mismatches_compatible_..._semver_group` |\n| unsafe semver group, already matches | `..._but_range_matches_incompatible_..._semver_group`      |\n| unsafe semver group, does not match  | `..._but_range_mismatches_incompatible_..._semver_group`   |\n\n---\n\n## Step 7: Verify commands need no changes\n\nCommands (`src/commands/*.rs`) operate on states generically:\n\n```rust\n// lint.rs, fix.rs etc. already handle your new variant automatically\ndependency.get_sorted_instances()\n    .filter(|instance| instance.is_invalid())  // or is_fixable(), is_unfixable() etc.\n    .for_each(|instance| { ... });\n```\n\nNew variants within existing categories (`Fixable`, `Unfixable`, etc.) are\npicked up automatically. You only need to touch command files if your new state\nrequires **bespoke fix logic** — for example, `IsBanned` requires removing the\ndependency entry rather than updating a version string.\n\n---\n\n## Step 8: Run the tests\n\n```bash\n# Run tests for a specific visitor\ncargo test same_minor\n\n# Run all tests\njust test\n```\n\n---\n\n## Checklist\n\n- [ ] Variant added to the correct enum in `src/instance_state.rs`\n- [ ] Doc comment uses `✓` `✘` `?` `!` bullets and describes every condition\n- [ ] Visitor uses `instance.mark_*()` methods — no direct state assignment\n- [ ] Per-dependency values computed once before the per-instance loop\n- [ ] Every instance is marked on every code path through the visitor\n- [ ] Tests cover every leaf of the decision tree\n- [ ] Tests use `expect(&ctx).to_have_instances(...)` with `ExpectedInstance`\n- [ ] No command changes needed (or bespoke fix logic added if required)\n\n---\n\n## Related files\n\n- `src/instance_state.rs` — all state variant definitions and annotations\n- `src/visit_packages.rs` — visitor dispatch\n- `src/visit_packages/*.rs` — visitor implementations\n- `src/visit_packages/*_test.rs` — test examples to copy patterns from\n- `src/commands/fix.rs` — example of how states drive fix behaviour\n- `.notes/reference/version-group-implementation-rules.md` — cross-cutting rules for all visitors\n",".claude/skills/document-code/SKILL.md":"---\nname: document-code\ndescription: Add and update the documentation website for Syncpack. Use when making user-facing changes to the codebase.\n---\n\n# Document Code\n\nGuide for adding and updating Syncpack's documentation website.\n\n## Quick Decision\n\nWhat are you documenting?\n\n| Adding/Updating...    | Location                                         |\n| --------------------- | ------------------------------------------------ |\n| Command docs          | `site/src/content/docs/command/[cmd].mdx`        |\n| Config property       | `site/src/content/docs/config/[prop].mdx`        |\n| Version group variant | `site/src/content/docs/version-groups/[var].mdx` |\n| Semver group variant  | `site/src/content/docs/semver-groups/[var].mdx`  |\n| Status code           | `site/src/content/docs/status/[code].mdx`        |\n| FAQ                   | `site/src/faq/[id].mdx`                          |\n| Shared option partial | `site/src/_partials/option/[opt].mdx`            |\n| Reusable component    | `site/src/components/[name].astro`               |\n\n## Workflow\n\n1. **Read source** — `cli.rs` for commands, `rcfile.rs` for config, `instance_state.rs` for status codes\n2. **Create/update MDX** — Match existing structure exactly\n3. **Add link identifier** — Update `globalReferenceLinks` in `astro.config.mjs`\n4. **Test locally** — `pnpm run dev` from `site/`\n5. **Verify links** — Check all `(IDENTIFIER)` links resolve\n\n## Source to Docs Mapping\n\n| Source                  | Documentation                                         |\n| ----------------------- | ----------------------------------------------------- |\n| `src/cli.rs` Subcommand | `site/src/content/docs/command/*.mdx`                 |\n| `src/rcfile.rs` Rcfile  | `site/src/content/docs/config/*.mdx`                  |\n| `src/instance_state.rs` | `site/src/content/docs/status/*.mdx`                  |\n| `src/specifier.rs`      | `site/src/content/docs/reference/specifier-types.mdx` |\n\n## Local Development\n\n```bash\ncd site\npnpm run dev          # Start at http://localhost:4321/syncpack\npnpm run build        # Verify build succeeds\n```\n\n## Site Overview\n\n- Source located at `site/src/`\n- Built with [Starlight](https://starlight.astro.build/) on top of [Astro](https://astro.build/)\n- Content at `site/src/content/docs/**/*.mdx`\n- Reusable partials at `site/src/_partials/**/*.mdx`\n- Reusable components at `site/src/components/*.astro`\n\n## Command Documentation\n\nEach command has a corresponding file at `site/src/content/docs/command/[command].mdx`.\n\n### Required Structure (in order)\n\n1. Description of the command's purpose from end user perspective\n2. `## Examples` — Single bash code block with commented examples\n3. `## Options` — Start with `<QuoteFilters />` callout, then `###` for each option alphabetically (`--help` last)\n\n### Shared Option Partials\n\nMany commands share options. Define reusable partials at `site/src/_partials/option/*.mdx`:\n\n```mdx\nimport ConfigOption from \"@partials/option/config.mdx\";\n\n<ConfigOption command=\"update\" />\n```\n\n## Config Documentation\n\nEach config property has a file at `site/src/content/docs/config/[property].mdx`.\n\n### Required Structure\n\n1. Description of the config's purpose from end user perspective\n2. `## Default Value` — Code block showing default (when applicable)\n3. `## Examples` — Code blocks with commented examples\n\n### Version Groups and Semver Groups\n\nEach variant has its own page:\n\n- `site/src/content/docs/version-groups/*.mdx`\n- `site/src/content/docs/semver-groups/*.mdx`\n\n#### Required Structure\n\n1. Description of the group's purpose from end user perspective\n2. `## Examples` — Code block per example, capture novel use cases\n3. `## Configuration` — `###` for each config property\n\n## Status Code Documentation\n\nEach status code has a file at `site/src/content/docs/status/[code].mdx`.\n\nReference page at `site/src/content/docs/reference/status-codes.mdx` explains the `InstanceState` enum and its nested enums (`ValidInstance`, `InvalidInstance`, `SuspectInstance`).\n\n## FAQ Documentation\n\n- Create FAQ files at `site/src/faq/[id].mdx`\n- Listed at `/syncpack/faq` via `site/src/pages/faq.astro`\n- Individual pages via `site/src/pages/faq/[id].astro`\n\n## Linking Between Pages\n\nUse named identifiers in markdown links:\n\n```mdx\nWhen using the [update](COMMAND_UPDATE) command\n```\n\nDefine identifiers in `globalReferenceLinks` function in `site/astro.config.mjs`.\n\n## Open Graph Images\n\nGenerated by `site/src/pages/og/[...path].png.ts` using [Satori](https://github.com/vercel/satori).\n\n## Troubleshooting\n\n| Problem                       | Fix                                           |\n| ----------------------------- | --------------------------------------------- |\n| Link not working              | Check identifier in `globalReferenceLinks`    |\n| Partial not rendering         | Verify import path uses `@partials/` alias    |\n| OG image broken               | Check `og/[...path].png.ts` handles new route |\n| Build fails                   | Run `pnpm run build` locally to see error     |\n| Page not appearing in sidebar | Check frontmatter and Starlight config        |\n\n## Checklist\n\nBefore submitting documentation:\n\n- [ ] Structure matches existing pages of same type\n- [ ] Link identifier added to `astro.config.mjs`\n- [ ] Local dev server renders correctly\n- [ ] Build succeeds (`pnpm run build`)\n- [ ] All internal links resolve\n",".claude/skills/fix-bug/SKILL.md":"---\nname: fix-bug\ndescription: Debug and fix bugs in Syncpack using scientific debugging methodology. Use when a test is failing, unexpected behaviour occurs, or investigating issues. Covers hypothesis-driven debugging and TDD-based fixes.\n---\n\n# Fix Bug\n\nGuide for debugging and fixing bugs in Syncpack.\n\n## Scientific Debugging Workflow\n\n1. **Observe** — Understand the symptom precisely\n2. **Hypothesise** — Form a theory about the root cause\n3. **Experiment** — Test the hypothesis with targeted changes\n4. **Validate** — Confirm the fix with tests\n5. **Verify** — Run full test suite\n\n## Step 1: Observe\n\nGather information before changing code:\n\n```bash\n# Run the failing test with output\ncargo test test_name -- --nocapture\n\n# Run with backtrace\nRUST_BACKTRACE=1 cargo test test_name\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- lint\n```\n\nQuestions to answer:\n\n- What is the exact error message?\n- What input triggers it?\n- What is the expected vs actual behaviour?\n\n## Step 2: Hypothesise\n\nCommon root causes by symptom:\n\n| Symptom              | Likely Cause                                            |\n| -------------------- | ------------------------------------------------------- |\n| State is `Unknown`   | `visit_packages` not called or instance skipped         |\n| Wrong state assigned | Validation logic order, earlier check overriding        |\n| Instance not found   | Package/dependency name mismatch, location format       |\n| Command not running  | Missing registration in cli.rs, main.rs, or commands.rs |\n| Panic/unwrap failure | Unexpected None or Err value                            |\n\n## Step 3: Experiment\n\nAdd targeted debug output:\n\n```rust\nuse log::debug;\n\ndebug!(\"Instance: {:#?}\", instance);\ndebug!(\"State before: {:?}\", instance.state.borrow());\n```\n\nSearch for related code:\n\n```bash\n# Find where state is assigned\nast-grep -p 'InstanceState::fixable' src/visit_packages/\n\n# Find similar patterns\nast-grep -p 'PATTERN' src/\n```\n\n## Step 4: Validate\n\nWrite a failing test FIRST (TDD):\n\n```rust\n#[test]\nfn reproduces_the_bug() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![/* minimal reproduction */])\n        .build_and_visit_packages();\n\n    // Assert expected behaviour (will fail initially)\n}\n```\n\nThen fix the code until the test passes.\n\n## Step 5: Verify\n\n```bash\njust test        # All tests pass\ncargo clippy     # No warnings\n```\n\n## Where to Look\n\n### By component\n\n| Issue with...    | Check...                                 |\n| ---------------- | ---------------------------------------- |\n| CLI parsing      | `src/cli.rs`                             |\n| Config loading   | `src/config.rs`, `src/rcfile.rs`         |\n| Package reading  | `src/packages.rs`, `src/package_json.rs` |\n| State assignment | `src/visit_packages/*.rs`                |\n| Command output   | `src/commands/*.rs`, `src/commands/ui/`  |\n| Version parsing  | `src/specifier/*.rs`                     |\n\n### By phase\n\n| Phase   | What happens                             | Files                                      |\n| ------- | ---------------------------------------- | ------------------------------------------ |\n| Create  | Read config, packages, collect instances | `context.rs`, `packages.rs`                |\n| Inspect | Assign InstanceState                     | `visit_packages.rs`, `visit_packages/*.rs` |\n| Run     | Process instances, output/write          | `commands/*.rs`                            |\n\n## Common Fixes\n\n### Instance state not being assigned\n\nCheck:\n\n1. Is the instance's version group variant handled in `visit_packages.rs`?\n2. Is there an early return skipping this instance?\n3. Is another validation assigning a state first?\n\n### Test assertion failing\n\nCheck:\n\n1. Is `build_and_visit_packages()` called (not just `build()`)?\n2. Is the location string format correct? (`\"{dep} in {location} of {package}\"`)\n3. Are there multiple instances of the same dependency?\n\n### Command not found\n\nVerify registration in all three places:\n\n1. `src/cli.rs` — `Subcommand` enum\n2. `src/main.rs` — match arm\n3. `src/commands.rs` — module declaration\n\n## Debugging Tips\n\n- **Isolate** — Create minimal reproduction\n- **Binary search** — Comment out code to narrow down\n- **Print early** — Add debug output before suspected area\n- **Check order** — Validation order matters (first match wins)\n- **Read tests** — Existing tests show expected behaviour\n\n## Anti-Patterns\n\n❌ Changing code without understanding the cause\n❌ Removing code just to make tests pass\n❌ Fixing symptoms instead of root cause\n❌ Skipping the test-first step\n",".claude/skills/front-loading/SKILL.md":"---\nname: front-loading\ndescription: Put the answer first, then context and details follow. Use when writing documentation, tutorials, error messages, or any communication where readers need to decide quickly if something is relevant to them.\n---\n\n# Front-Loading\n\nPut the answer first. Context follows.\n\n## What This Is\n\nFront-loading means leading with the most important information. Users decide in the first few words whether to keep reading.\n\nStructure your communication as:\n\n1. **The point** (first sentence)\n2. **Why it matters** (next 1–2 sentences)\n3. **Details and evidence** (rest of content)\n\nMost writers do the opposite: context first, then conclusion. Readers get lost in the journey before reaching the destination.\n\n## When to Use\n\n- Writing documentation or guides\n- Structuring arguments or explanations\n- Creating tutorial sections\n- Writing error messages\n- Composing email or feedback\n- Organizing any sequential information\n\nFront-load at every level: sentences, paragraphs, sections, documents.\n\n## How to Apply\n\n### 1. Lead with the Conclusion\n\nPut the answer first. Don't make readers wait.\n\n**Bad (context first):**\n\"There are many different ways to approach testing. Some teams test before writing code. Others test after. Some tests run quickly, others take longer. The most important thing is consistency. So you should write tests.\"\n\n**Good (conclusion first):**\n\"Write tests. Consistency matters more than approach.\"\n\nThen explain the trade-offs if needed.\n\n### 2. Answer the Reader's Question Immediately\n\nReaders have a question in mind. Answer it in the first sentence.\n\n**Bad (makes reader wait):**\n\"Syncpack is a CLI tool built in Rust. It uses a configuration file in JSON format. The configuration file specifies rules for how to synchronise dependencies. These rules can be customized per workspace...\"\n\nReader's question: \"How do I configure Syncpack?\" Buried in paragraph 3.\n\n**Good (answers immediately):**\n\"Configure Syncpack in `.syncpackrc.json` at your monorepo root. Specify version groups and rules for each.\"\n\nThen explain each option.\n\n### 3. Use Signposting\n\nTell readers what's coming before they read it.\n\n**Bad (no preview):**\n\"First, install Node 18. Then, verify the installation. Next, clone the repository. After that, install dependencies...\"\n\nReader: \"Where is this going?\"\n\n**Good (preview structure):**\n\"Three steps: Install Node, clone the repo, and run `npm install`.\"\n\nThen detail each step.\n\n### 4. When Explaining a Process, Show the End State First\n\nLet readers know what success looks like before they start.\n\n**Bad (journey without destination):**\n\"Follow these 12 steps to set up CI/CD. Step 1: Create a workflow file. Step 2: Add a trigger. Step 3: Define a job...\"\n\nReader: \"Why am I doing this?\"\n\n**Good (end state first):**\n\"Goal: Your PR automatically runs tests and fails if any break. Here's how to set it up in 12 steps.\"\n\nThen detail steps 1–12.\n\n### 5. Structure Paragraphs with Topic Sentence First\n\nEach paragraph's first sentence should be its main idea.\n\n**Bad (idea buried):**\n\"There are many different caching strategies. Some store values in memory. Others use disk. Still others use remote services. The strategy you choose depends on your use case. For most applications, in-memory caching is sufficient.\"\n\nReader has to parse the whole paragraph to find the point.\n\n**Good (topic first):**\n\"For most applications, in-memory caching is sufficient. Other strategies (disk, remote) exist for specific cases. Choose based on your performance and persistence needs.\"\n\n### 6. When Providing Options, Lead with the Recommendation\n\nDon't make readers compare three options to find the best one.\n\n**Bad (no guidance):**\n\"You can use approach A, B, or C. Each has trade-offs. A is simple but inflexible. B is flexible but complex. C is somewhere in between. Most projects use B or C.\"\n\nReader: \"So which should I use?\"\n\n**Good (recommendation first):**\n\"Use approach B (flexible but complex). It scales as your needs grow. Use approach A only if simplicity is critical. Approach C rarely adds value.\"\n\n## Examples\n\n### Documentation Section\n\n**Bad (context first):**\n\"The registry client handles communication with npm. Over time, performance became an issue. We realized the client was making unnecessary requests. After analysis, we found that caching solved the problem. So the registry client now caches responses.\"\n\n**Good (point first):**\n\"The registry client caches npm responses to improve performance. This reduced request overhead by 60%.\"\n\nThen explain the details.\n\n---\n\n### Error Message\n\n**Bad (generic context):**\n\"An operation has encountered an error condition. The system attempted to perform a task. This task requires certain prerequisites. One or more prerequisites were not met. Please verify your setup.\"\n\n**Good (specific answer):**\n\"Missing required field in config: 'versionGroups'. See `.syncpackrc.json` example.\"\n\n---\n\n### Tutorial Section\n\n**Bad (setup before goal):**\n\"Before you can run Syncpack, you need to set up your monorepo structure. This involves creating a root directory. Inside this directory, you need workspace packages. Each package should have a `package.json`. Once you have this structure, you can run Syncpack.\"\n\n**Good (goal first):**\n\"Goal: Run Syncpack to sync versions across your monorepo.\"\n\nThen list the prerequisites (monorepo structure, workspace packages).\n\n---\n\n### Workflow Documentation\n\n**Bad (steps without outcome):**\n\"Step 1: Write your implementation. Step 2: Write tests. Step 3: Run tests locally. Step 4: Push to GitHub. Step 5: Wait for CI. Step 6: Address failures. Step 7: Merge when green.\"\n\n**Good (outcome first):**\n\"Goal: Merge code only when tests pass and CI approves.\"\n\nThen list the steps to reach that goal.\n\n## Trade-offs\n\n### When NOT to front-load\n\n- **Narrative/storytelling:** Sometimes the journey is the point. Build suspense if appropriate.\n- **Teaching from first principles:** Some explanation is necessary before jumping to conclusions.\n- **Building trust:** Sometimes context _is_ the point (e.g., \"here's why we made this decision\").\n- **Sensitive feedback:** Sometimes leading with criticism is harmful. Lead with understanding instead.\n\nThe principle: **Match the reader's mental model.** If they need context to understand the conclusion, provide context first.\n\n### Common mistake\n\nOver-front-loading: \"Use this. End of story.\" without any reasoning alienates readers who need understanding.\n\nSolve by asking: \"Will the reader understand why this conclusion is right?\" If no, provide enough context. But lead with the conclusion, not the context.\n\n## Testing\n\nHow to know if you've front-loaded effectively:\n\n1. **First sentence test:** Can someone read only the first sentence and understand the main point?\n2. **Skim test:** Someone skims headings and bullets—do they get the gist?\n3. **Reader expectation test:** \"What happens next?\" readers predict after each section. Were they right?\n4. **Reduction test:** Remove everything after the first paragraph. Is the main point clear? (If yes, you front-loaded successfully.)\n5. **Fresh eyes test:** Have someone read the first paragraph and summarize. Did they get it right?\n\n## Application in Syncpack\n\nFront-loading improves:\n\n- **Skill descriptions:** Users know instantly if it's relevant\n- **Error messages:** Users understand what failed and how to fix it\n- **Documentation sections:** Readers find what they need faster\n- **Code comments:** Maintainers understand the \"why\" before the \"how\"\n- **Commit messages:** Reviewers know the intent before reading code\n- **Proposals or plans:** Stakeholders see the recommendation before the analysis\n\nEvery document you write is a race against the reader's attention span. Front-load to win that race.\n",".claude/skills/optimize-benchmarks/SKILL.md":"---\nname: optimize-benchmarks\ndescription: Iterative performance optimisation loop for syncpack-specifier. Runs benchmarks, identifies bottlenecks, applies optimisations, verifies tests pass and benchmarks improve, then repeats.\n---\n\n# Optimize Benchmarks\n\nIterative loop: benchmark, optimise, test, verify improvement, repeat.\n\n## Workflow\n\n### 1. Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --save-baseline before 2>&1 | tail -40\n```\n\nSave the output. Identify which variants are slowest.\n\n### 2. Identify Bottleneck\n\nFrom the full baseline, focus on the **slowest** benchmarks first.\n\nPriority order for specifier parsing:\n\n1. `Specifier::create` — the main parse function, called for every version string\n2. `parser::is_range` — checks 12 regexes sequentially\n3. `parser::is_exact` — checks 4 regexes sequentially\n4. `parser::is_complex_range` — splits, collects, iterates\n5. Individual regex matches in `regexes.rs`\n\n### 3. Apply ONE Optimisation\n\nMake a **single, focused change**. Do NOT bundle multiple optimisations — each must be independently measurable.\n\n### 4. Verify Tests Pass\n\n```bash\ncargo test -p syncpack-specifier 2>&1 | tail -5\n```\n\nIf tests fail, fix or revert. Never proceed with failing tests.\n\n### 5. Benchmark Against Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --baseline before 2>&1 | tail -40\n```\n\nLook for `[-XX.XXX% ...]` (improvement) or `[+XX.XXX% ...]` (regression).\n\n### 6. Evaluate\n\n- **Improved:** Report the gains. Update baseline: `cargo bench -p syncpack-specifier -- --save-baseline before`. Continue to step 2.\n- **No change:** Revert and try a different approach.\n- **Regressed:** Revert immediately.\n\n### 7. Repeat\n\nGo to step 2. Stop when:\n\n- User says stop\n- No bottlenecks remain\n- Gains are <1% across all benchmarks\n\n## Known Optimisation Opportunities\n\n### High Impact\n\n**Replace regex with char-based parsing in `parser.rs` / `regexes.rs`**\n\nMost regexes in `regexes.rs` match simple patterns like `^[0-9]+\\.[0-9]+\\.[0-9]+$` (exact semver). These can be replaced with byte/char iteration:\n\n```rust\n// Instead of regex EXACT: r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\"\nfn is_exact_version(s: &str) -> bool {\n  let mut dots = 0;\n  let bytes = s.as_bytes();\n  if bytes.is_empty() { return false; }\n  for &b in bytes {\n    match b {\n      b'0'..=b'9' => {},\n      b'.' => dots += 1,\n      _ => return false,\n    }\n  }\n  dots == 2\n}\n```\n\nRegex `is_match()` has overhead even for simple patterns: engine setup, capture group allocation. Char-based parsing for these patterns is 5-20x faster.\n\n**Reduce sequential regex attempts in `parser::is_range`**\n\n`is_range` tries 12 regexes. Instead, match on first char(s) to dispatch:\n\n```rust\nfn is_range(s: &str) -> bool {\n  match s.as_bytes().first() {\n    Some(b'^') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'~') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'>') => { /* check >= vs > then validate remainder */ },\n    Some(b'<') => { /* check <= vs < then validate remainder */ },\n    _ => false,\n  }\n}\n```\n\n**Consolidate related regex patterns**\n\nMany regexes are pairs: `EXACT` + `EXACT_TAG`, `CARET` + `CARET_TAG`, etc. Merge each pair into one function that handles both cases:\n\n```rust\nfn is_exact(s: &str) -> bool {\n  // Parse digits.digits.digits, then optionally -tag\n  let rest = parse_semver_triple(s)?;\n  rest.is_empty() || rest.starts_with('-')\n}\n```\n\n### Medium Impact\n\n**Replace `lazy_static` with `std::sync::OnceLock`**\n\n`lazy_static` uses an extra indirection layer. `OnceLock` (stable since Rust 1.80) is zero-cost after init:\n\n```rust\nuse std::sync::OnceLock;\n\nfn exact_regex() -> &'static Regex {\n  static RE: OnceLock<Regex> = OnceLock::new();\n  RE.get_or_init(|| Regex::new(r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\").unwrap())\n}\n```\n\nBut if regex is being replaced with char-based parsing, this becomes irrelevant.\n\n**Reorder checks in `Specifier::create` by frequency**\n\nIn a typical monorepo, most specifiers are `^x.y.z` (range) or `x.y.z` (exact). The current order already checks exact first, then range — good. But `is_exact` tries 4 regex patterns. A single fast char check can short-circuit:\n\n```rust\n// Fast path: first char is digit → likely exact or major or minor\n// Fast path: first char is ^ or ~ → likely range\n```\n\n**Avoid `String` allocation in `strip_semver_range`**\n\n`strip_semver_range` returns `&str` (already good), but callers like `Range::create` then `.to_string()` the result. Consider whether the allocation can be deferred.\n\n### Low Impact\n\n- Replace `HashMap` in caches with `FxHashMap` (faster hashing for short strings)\n- Use `SmallString` or stack-allocated strings for short specifiers\n- Pre-size cache `HashMap` with expected capacity\n\n## Architecture Notes\n\nKey files in `crates/syncpack-specifier/src/`:\n\n| File                         | Role                                                        |\n| ---------------------------- | ----------------------------------------------------------- |\n| `lib.rs`                     | `Specifier` enum, `create()` dispatch, caches               |\n| `parser.rs`                  | `is_exact()`, `is_range()`, etc. — classification functions |\n| `regexes.rs`                 | All `lazy_static` regex patterns                            |\n| `exact.rs`, `range.rs`, etc. | Variant constructors calling `node_semver`                  |\n| `semver_range.rs`            | `SemverRange` enum, `parse()`                               |\n\nThe hot path is: `Specifier::create()` → `parser::is_*()` → `regexes::*` → variant `::create()` → `node_semver` parsing.\n\nOptimising the `parser::is_*` layer gives the biggest wins because it runs for **every** specifier, and most of the time most checks return `false` (only one branch matches).\n\n## Rules\n\n- ONE change per iteration\n- Always verify tests pass before benchmarking\n- Always compare against baseline\n- Report numbers: `before → after (% change)`\n- Revert regressions immediately\n- Don't optimise what doesn't show up in benchmarks\n- Use **fast iteration** (`\"batch\"` filter) during the loop, **full suite** only at start and end\n",".claude/skills/review-docs/SKILL.md":"---\nname: review-docs\ndescription: Review and improve Syncpack documentation for clarity, completeness, and consistency. Use when enhancing docs or validating before publication.\n---\n\n# Review Docs\n\nReview and improve Syncpack documentation against project standards.\n\n## When to Use\n\n- Validating docs before publication\n- Improving existing documentation\n- Ensuring consistency across pages\n- User reports documentation is unclear\n\n## Related Skills\n\nUse with:\n\n- `document-code` — Reference for structure and locations\n- `signal-over-noise` — Apply to remove filler and obvious explanations\n- `front-loading` — Ensure actionable information comes first\n\n## Your Task\n\nGiven a documentation path, review content against standards. Suggest concrete improvements.\n\n## Quality Checklist\n\n- [ ] **Clarity**: First line immediately shows what it covers\n- [ ] **Examples**: Practical, show real use cases, cover major patterns\n- [ ] **Completeness**: All relevant aspects documented\n- [ ] **Consistency**: Formatting matches other docs\n- [ ] **Conciseness**: No fluff; every sentence earns space\n- [ ] **Accessibility**: Users find what they need without deep reading\n- [ ] **Reuse**: Reference shared partials when available\n\n## Review Process\n\n1. Read the documentation file\n2. Cross-reference structure with similar docs\n3. Identify information gaps where users might get stuck\n4. Check for shared partials at `site/src/_partials/`\n5. Apply signal-over-noise: Can you cut 20% of words without losing meaning?\n\n## Review Output\n\nProvide structured feedback:\n\n**Current State**\n\n- Assessment of documentation quality and coverage\n\n**Issues Found**\n\n- Specific problems with locations (section names or line references)\n- Prioritize by impact: clarity > completeness > polish\n\n**Recommendations**\n\n- Concrete improvements with examples\n- Rewrite short sections if needed\n- Flag missing content or examples\n- Suggest shared partials to reuse\n\n**Priority Actions**\n\n- Top 2-3 changes to make immediately\n\n## Reference\n\n**Docs:** `site/src/content/docs/`\n**Shared partials:** `site/src/_partials/`\n**Source of truth:** `src/` (Rust implementation)\n**Local testing:** `cd site && pnpm run dev` then verify at `http://localhost:4321/syncpack`\n",".claude/skills/search-code/SKILL.md":"---\nname: search-code\ndescription: Search for code patterns in Syncpack. Use when finding symbols, implementations, or understanding how code is used. Covers ast-grep for Rust and grep/rg for other cases.\n---\n\n# Search Code\n\nGuide for searching the Syncpack codebase effectively.\n\n## Golden Rule\n\n**Use `ast-grep` for Rust files** — it filters out comments, strings, and docs.\n\n## Quick Reference\n\n| Looking for...                    | Use         |\n| --------------------------------- | ----------- |\n| Rust symbols, patterns, structs   | `ast-grep`  |\n| Multiple file types               | `grep`/`rg` |\n| Content in comments intentionally | `grep`/`rg` |\n| Non-Rust files (JSON, MD, TOML)   | `grep`/`rg` |\n\n## ast-grep Examples\n\n```bash\n# Find struct definitions\nast-grep -p 'pub struct $NAME' src/\n\n# Find function calls\nast-grep -p 'Specifier::new' src/\n\n# Find enum variants\nast-grep -p 'pub enum $NAME' src/\n\n# Find impl blocks\nast-grep -p 'impl $TYPE' src/\n\n# Find specific method definitions\nast-grep -p 'pub fn run($$$) -> i32' src/\n\n# Find match expressions\nast-grep -p 'match $EXPR { $$$ }' src/\n```\n\n## Pattern Syntax\n\n| Pattern | Matches                                      |\n| ------- | -------------------------------------------- |\n| `$NAME` | Single identifier                            |\n| `$$$`   | Zero or more items (arguments, fields, etc.) |\n| `$_`    | Any single expression                        |\n\n## When to Use grep/rg\n\n```bash\n# Search across file types\nrg \"TODO\" --type rust --type md\n\n# Search in comments (ast-grep ignores these)\nrg \"FIXME\" src/\n\n# Search non-Rust files\nrg \"version\" Cargo.toml\n\n# Case-insensitive search\nrg -i \"error\" src/\n```\n\n## Common Searches\n\n### Find where a type is used\n\n```bash\nast-grep -p 'Context' src/\n```\n\n### Find all InstanceState assignments\n\n```bash\nast-grep -p 'InstanceState::valid' src/\nast-grep -p 'InstanceState::fixable' src/\nast-grep -p 'InstanceState::unfixable' src/\n```\n\n### Find test files\n\n```bash\nfind src -name '*_test.rs'\n```\n\n### Find command implementations\n\n```bash\nast-grep -p 'pub fn run($$$) -> i32' src/commands/\n```\n\n## Tips\n\n- Start broad, then narrow with more specific patterns\n- Use `ast-grep` output to find the file, then `read_file` to understand context\n- Chain searches: find the type, then find where it's constructed\n",".claude/skills/signal-over-noise/SKILL.md":"---\nname: signal-over-noise\ndescription: Maximize useful information per word by removing filler, obvious explanations, and hedging language. Use when writing documentation, error messages, code comments, or any communication where clarity and conciseness matter.\n---\n\n# Signal Over Noise\n\nMaximize useful information per word. Remove everything that doesn't earn its space.\n\n## What This Is\n\nSignal = information that moves the reader toward action or understanding.\nNoise = filler, explanations of obvious things, hedging language, unnecessary context.\n\nHigh signal-to-noise ratio means readers get what they need faster. Every word counts.\n\n## When to Use\n\n- Writing documentation, guides, skill descriptions\n- Creating error messages and feedback\n- Structuring arguments or explanations\n- Naming things (variables, functions, branches)\n- Any communication where clarity matters\n\n## How to Apply\n\n### 1. Remove Filler Words\n\n**High noise:**\n\n- \"basically\"\n- \"essentially\"\n- \"in order to\"\n- \"comprehensive\"\n- \"helpful\"\n- \"I think that\"\n- \"it could be argued that\"\n\n**Before:** \"Basically, you should essentially use this pattern in order to get comprehensive error handling.\"\n\n**After:** \"Use this pattern for error handling.\"\n\n### 2. Cut Explanations of Obvious Things\n\nDon't explain what readers already know. Assume competence.\n\n**Before:** \"Rust is a programming language. In Rust, you write code. This code runs on computers. When you compile Rust code, it produces a binary executable file.\"\n\n**After:** \"Compile Rust code to a binary with `cargo build --release`.\"\n\n### 3. Front-Load the Point\n\nPut the actionable information first. Context follows.\n\n**Before:** \"There are several reasons why you might want to consider testing your code. First, testing helps catch bugs. Second, it makes refactoring safer. Most importantly, tests serve as documentation. You should write tests.\"\n\n**After:** \"Write tests to catch bugs, enable safe refactoring, and document behavior.\"\n\n### 4. Use Active, Direct Language\n\n**High noise:** \"It is recommended that consideration be given to...\"\n\n**Signal:** \"Consider...\"\n\n**High noise:** \"The file was created by the user.\"\n\n**Signal:** \"Create the file.\"\n\n### 5. One Idea Per Sentence\n\nLong sentences hide the point. Break them.\n\n**Before:** \"When you're writing code that handles errors, which can happen in many places throughout your application, you should think about whether you want to handle them at the point where they occur or propagate them upward, and this decision affects how you structure your code.\"\n\n**After:** \"Handle errors where they occur or propagate them upward. This choice shapes your code structure. Decide early.\"\n\n### 6. Name Things Precisely\n\nNames are communication. Bad names add noise; good names clarify.\n\n**High noise:** `process_data()`, `handle_thing()`, `do_stuff()`\n\n**Signal:** `validate_user_input()`, `retry_failed_request()`, `parse_json_config()`\n\n### 7. Omit Context Readers Already Have\n\nDon't repeat what's obvious from surrounding text.\n\n**Before:** \"Here's an example of how to use the feature. In this example, we show you how to use it.\"\n\n**After:** \"Example:\"\n\n## Trade-offs\n\n### When NOT to apply aggressively\n\n- **Teaching beginners:** Some explanatory redundancy helps them build intuition\n- **Safety-critical systems:** Explicit redundancy catches misunderstanding\n- **Legal/compliance docs:** Specificity sometimes requires verbose phrasing\n- **User research notes:** Capture raw data, organize later\n\n### The balance\n\nSignal-over-noise isn't \"be terse.\" It's \"be useful.\" Sometimes that means:\n\n- Repeating a key phrase for emphasis\n- Providing context that prevents misreading\n- Showing your work so readers can verify reasoning\n- Acknowledging limitations explicitly\n\nRemove noise, keep signal. Don't remove useful context.\n\n## Examples\n\n### Code Documentation\n\n**Before:**\n\n```\nThis function is useful for validating email addresses.\nIt takes a string as input, which represents the email address\nthat you want to check. The function will return true if the\nemail is valid, or false if it is not valid. You might want to\nuse this function when you need to validate emails.\n```\n\n**After:**\n\n```\nValidate email addresses. Returns true if valid, false otherwise.\n```\n\n### Error Messages\n\n**Before:**\n\n```\nAn error has occurred in the system. The operation you attempted\ncould not be completed successfully. Please try again later or\ncontact support if the problem persists.\n```\n\n**After:**\n\n```\nFailed to fetch user data. Retry or contact support.\n```\n\n### Skill Description\n\n**Before:**\n\n```\nThis skill helps you think about how to communicate more effectively\nby using techniques that are designed to help you reduce unnecessary\ninformation and focus on the important parts of what you're trying\nto say so that your readers understand your message better and faster.\n```\n\n**After:**\n\n```\nMaximize useful information per word. Remove filler, obvious explanations, and hedging language. Every word should earn its space.\n```\n\n## Testing\n\nHow to know if you've applied this principle:\n\n1. **Read it aloud.** Does every sentence move you forward?\n2. **Ask \"why?\"** for each phrase. If the answer is \"readers already know this,\" cut it.\n3. **Count words.** Did you reduce the word count by 20–40%? Good sign.\n4. **Test comprehension.** Can a fresh reader understand it without re-reading?\n5. **Check names.** Are function/variable names predictable and specific?\n\n## Application in Syncpack\n\nThis principle strengthens all your other skills:\n\n- **write-code:** Function names, comments, commit messages all benefit\n- **document-code:** Docs communicate faster when noise is removed\n- **add-feature:** Feature descriptions and changelogs stay concise\n- **fix-bug:** Error messages become actionable, not explanatory\n- **search-code:** Clear naming makes code more discoverable\n\nApply signal-over-noise across the board. Your CLAUDE.md already mandates it—this skill formalizes how.\n",".claude/skills/testing-gaps/SKILL.md":"---\nname: testing-gaps\ndescription: Run coverage, inspect results, and identify missing test scenarios for a given source file. Use when analysing test coverage or finding untested branches.\n---\n\n# Testing Gaps\n\nFind untested branches and missing real-world test scenarios for a source file.\n\n## Workflow\n\n1. **Run text coverage** for the target file\n2. **Identify uncovered branches** from the output\n3. **Cross-reference** with test file to understand what's tested\n4. **Report** uncovered branches + missing real-world scenarios\n\n## Step 1: Run Coverage\n\nUse `cargo llvm-cov` with `--text` output (not `--html`) for parseable results.\n\n```bash\n# Full coverage, filtered to target file\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/TARGET_FILE\\.rs:/,/^$/p'\n```\n\nExample for `preferred_semver.rs`:\n\n```bash\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/preferred_semver\\.rs:/,/^$/p'\n```\n\n## Step 2: Read Coverage Output\n\nThe text format shows per-line execution counts:\n\n```\n  42|     80|        .and_then(|range| ...)     # Hit 80 times\n  74|      0|        Some(preferred)            # Never hit\n 321|      0|        instance.mark_conflict(...) # Never hit\n```\n\n- `count > 0` = covered\n- `count = 0` = uncovered branch\n- `^N` annotations on sub-expressions show partial coverage within a line\n\n### Extract only uncovered lines\n\n```bash\n... | grep -E '^\\s+\\d+\\|\\s+0\\|'\n```\n\n## Step 3: Classify Gaps\n\nFor each uncovered branch, determine:\n\n1. **What code path leads here?** — Trace the `if/else` chain backward\n2. **What input would trigger it?** — What package.json + config combination\n3. **Is it reachable?** — Some branches may be defensive/unreachable\n4. **Is it worth testing?** — Real-world scenario vs theoretical edge case\n\n### Categories\n\n| Category                         | Action            |\n| -------------------------------- | ----------------- |\n| Real-world scenario never tested | Write a test      |\n| Edge case in existing logic      | Write a test      |\n| Defensive branch (unreachable)   | Note but skip     |\n| Dead code                        | Consider removing |\n\n## Step 4: Identify Missing Scenarios\n\nBeyond line coverage, look for missing _combinations_:\n\n- Feature A tested, Feature B tested, A+B never tested together\n- Only tested with one package, never with multiple\n- Only tested with `highestSemver`, never `lowestSemver`\n- Only tested with one range type (`^`), never others (`>=`, `<=`)\n- Only tested without semver groups, never with\n\n## Step 5: Report\n\nStructure findings as:\n\n1. **Uncovered branches** — Specific lines, what triggers them, why untested\n2. **Missing real-world scenarios** — Combinations and interactions not covered\n\n## Running Tests After Adding Coverage\n\n```bash\njust test                              # All tests\ncargo test test_name -- --nocapture   # Specific test\njust coverage                         # Regenerate coverage report\n```\n",".claude/skills/write-code/SKILL.md":"---\nname: write-code\ndescription: Rust code style and conventions for Syncpack. Use when writing or modifying Rust code. Covers functional patterns, imports, naming, and quality standards.\n---\n\n# Write Code\n\nRust code conventions for Syncpack.\n\n## Style\n\n- **Functional style:** pipelines over loops\n- **Avoid `?` chains:** use `.and_then()`, `.map()`, `.or_else()`\n- **Descriptive names:** clarity over brevity\n- **Named placeholders:** `println!(\"{var}\")` not `println!(\"{}\", var)`\n- **British English:** \"behaviour\" not \"behavior\", \"organised\" not \"organized\"\n\n## Imports\n\nSingle `use` statement with grouped braces:\n\n```rust\nuse {\n  crate::{cli::Cli, config::Config},\n  log::{debug, error},\n  std::{process::exit, sync::Arc},\n};\n```\n\nRules:\n\n- **Never use `super::`** — always `crate::` for internal imports\n- Group: `crate::`, external crates, `std::`\n- Alphabetise within groups\n\n## File Organisation\n\n| Adding...   | Location                                                         |\n| ----------- | ---------------------------------------------------------------- |\n| New command | `src/commands/{name}.rs`                                         |\n| New test    | Sibling `_test.rs` file (e.g., `src/foo.rs` → `src/foo_test.rs`) |\n\n**NEVER** use `#[cfg(test)]` modules inside implementation files.\n\n## Quality\n\n- Functions <50 lines, commands 100-300 lines\n- Zero warnings (except during TDD red phase)\n- Run `just format` before committing\n\n## Comments\n\n### Forbidden\n\n- **`//!` module docs** — collect implementation history and rot\n- **Phase/F-N labels** — `// Phase v4-2`, `// F16:`, banner headers like `// ---------- Phase v4-3 RED ----------`\n- **History refs** — `Migrated from`, `carry-over`, `Mirrors v3`, `replaces the v3`, `previously inside X`, `reserved for future`\n- **Issue/PR refs** — `Reproduces issue #239`, `GitHub issue #206`. Plans rot; the code is the truth\n- **Banner separators** — `// ---------- Free functions: yaml ops ----------`. Use module structure or let the file speak for itself\n- **Test scenario labels** — don't write `// Windows-style backslashes` above a test input. Use a descriptive variable binding (`let windows_backslashes = [...]`) instead\n- **Name-restating field docs** — `/// A unique identifier for this instance`, `/// The dependency name`, `/// The instance id`. The field name already says it\n- **Step numbering inside functions** — `// 1. ...`, `// 2. ...`. Drop the numbers; keep the WHY if it's non-obvious\n\n### Allowed\n\n- **`@TODO` markers** — future plans worth tracking inline\n- **`///` doc comments** for non-obvious detail: `None`/`Err` semantics, side effects, invariants, distinctions between similar fields (e.g. `is_local_dependency` vs `is_local_instance`)\n- **`//` inline comments** when WHY is non-obvious: hidden constraints, ordering invariants (\"SnappedTo groups must be visited last\"), workarounds for surprising library behaviour, \"must not poison X\" cautions\n\n### Default\n\nNo comments. Add one only when the WHY is non-obvious to a fresh reader.\n\n## Patterns\n\n### Iterating Instances\n\n```rust\nctx.version_groups.iter().for_each(|group| {\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n        dependency.get_sorted_instances()\n            .filter(|instance| instance.is_invalid())\n            .for_each(|instance| { /* process */ });\n    });\n});\n```\n\n### Error Handling\n\nPrefer combinators over `?`:\n\n```rust\n// Good\npath.parent()\n    .and_then(|p| p.to_str())\n    .map(|s| s.to_string())\n    .unwrap_or_default()\n\n// Avoid\nlet parent = path.parent()?;\nlet str = parent.to_str()?;\nOk(str.to_string())\n```\n\n### State Mutation\n\n```rust\nlet mut state = instance.state.borrow_mut();\nif !state.is_invalid() {\n    *state = InstanceState::fixable(SomeVariant);\n}\n```\n",".claude/skills/write-tests/SKILL.md":"---\nname: write-tests\ndescription: Write tests for Syncpack using the TestBuilder pattern. Use when adding tests for commands, validation logic, or any new functionality. Covers TestBuilder API, assertion patterns, and common test scenarios.\n---\n\n# Write Tests\n\n## Golden rules\n\n- Use `TestBuilder` — never construct `Context` manually.\n- Assert with `expect(&ctx).to_have_instances(vec![...])` — never index `ctx.instances`.\n- Async-first: `#[tokio::test]` + `.run().await` is the canonical entry.\n\n## TDD\n\n1. Read 2-3 tests in the same `*_test.rs` and copy the pattern\n2. Write failing test → `just test` → confirm RED\n3. Ask before implementing\n4. Implement minimal code → GREEN → `just format`\n\n## Quick start\n\n```rust\nuse {\n  crate::{\n    instance::{FixableInstance::*, InstanceState, SuspectInstance::*, UnfixableInstance::*, ValidInstance::*},\n    test::{\n      builder::TestBuilder,\n      expect::{expect, ExpectedInstance},\n    },\n  },\n  serde_json::json,\n};\n\n#[tokio::test]\nasync fn pinned_version_replaces_anything_different() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"workspace:*\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"workspace:*\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Sub-module organisation\n\nGroup related scenarios under nested modules — see `pinned_test.rs`, `catalog_defs_test.rs`:\n\n```rust\nmod local {\n  use super::*;\n  #[tokio::test] async fn refuses_to_pin_local_version() { ... }\n}\n\nmod normal {\n  use super::*;\n  #[tokio::test] async fn an_already_pinned_version_is_valid() { ... }\n}\n\nmod registry_updates {\n  use super::*;\n  #[tokio::test] async fn def_marked_outdated_when_registry_has_newer_version() { ... }\n}\n```\n\n## File location\n\n| Test type                                    | Location                                    |\n| -------------------------------------------- | ------------------------------------------- |\n| Version-group behaviour (pin, ban, ranges …) | `src/version_group/<group>_test.rs`         |\n| Catalog discovery wiring                     | `src/version_group/{catalog,bun_catalog}_test.rs` |\n| Fix mutations                                | `src/commands/fix_test.rs`                  |\n| Format pass                                  | `src/visit_formatting/format_test.rs`       |\n| Other unit tests                             | Co-located: `src/foo.rs` ↔ `src/foo_test.rs` |\n\n## Builder methods\n\nSource of truth: `src/test/builder.rs`.\n\n| Method                                                  | Purpose                                                              |\n| ------------------------------------------------------- | -------------------------------------------------------------------- |\n| `.with_package(json!({...}))`                           | Add one package.json                                                 |\n| `.with_packages(vec![...])`                             | Add many                                                             |\n| `.with_version_group(json!({...}))`                     | Add one version group                                                |\n| `.with_version_groups(vec![...])`                       | Add many                                                             |\n| `.with_semver_group(json!({...}))`                      | Add a semver group                                                   |\n| `.with_config(json!({...}))`                            | Base config (e.g. `customTypes`, `dependencyGroups`)                 |\n| `.with_strict(bool)`                                    | Strict mode (Suspect → error)                                        |\n| `.with_subcommand(\"update\")`                            | Override subcommand (default: `lint`, or `update` if registry set)   |\n| `.with_pnpm_catalogs(yaml)`                             | Inject `pnpm-workspace.yaml`; implies pnpm PM                        |\n| `.with_bun_catalogs(json!({...}))`                      | Synthetic Bun root with `/catalog`, `/catalogs/{n}`; implies Bun PM  |\n| `.with_bun_workspaces_catalogs(json!({...}))`           | Same, nested under `/workspaces/`                                    |\n| `.with_{pnpm,bun,npm,yarn,unknown}_package_manager()`   | Force PM detection                                                   |\n| `.with_registry_updates(json!({\"react\":[...]}))`        | Mock npm registry; implies subcommand=`update`                       |\n| `.with_update_target(UpdateTarget::Minor)`              | Bound update target                                                  |\n| `.run().await` → `Context`                              | **Primary** end-to-end (full pipeline through disk + discovery)      |\n| `.build()`                                              | Sync, no visit — context-wiring tests                                |\n| `.build_and_visit_packages()`                           | Sync + visit_packages — fix tests, older suites                      |\n| `.build_and_visit_formatting()`                         | Sync + visit_formatting                                              |\n| `.build_with_registry_and_visit().await`                | Sync wiring + async registry mock + visit                            |\n\n## ExpectedInstance fields\n\n```rust\nExpectedInstance {\n  state: InstanceState::fixable(DiffersToPin),  // valid / fixable / unfixable / suspect\n  dependency_name: \"react\",                     // = `internal_name` (alias-aware)\n  id: \"react in /dependencies of package-a\",    // {dep} in {/path} of {package_or_yaml}\n  actual: \"17.0.0\",                             // raw specifier on disk\n  expected: Some(\"18.0.0\"),                     // None = ignore; Some(\"\") = remove\n  overridden: None,                             // semver-group override target, if any\n}\n```\n\n`id` location examples:\n\n- `/dependencies`, `/devDependencies`, `/peerDependencies`\n- `/version of package-a` (local version)\n- `/packageManager`, `/engines/node`\n- `/catalog of pnpm-workspace.yaml`, `/catalogs/<name> of pnpm-workspace.yaml`\n- `/customVersion`, `/custom/config/version` (via `customTypes`)\n\n## Patterns\n\n→ [patterns.md](patterns.md): banned, pinned, sameRange, pnpm catalogs, bun catalogs, semver ranges, registry updates.\n\n## Fix tests\n\n`src/commands/fix_test.rs` builds with `.build_and_visit_packages()` then runs `fix::run(ctx, &SilentReporter, &disk)`. `dry_run = true` is the default (set by `mock::config_from_mock`), so `is_dirty()` and post-fix contents stay observable. Set `ctx.config.cli.dry_run = false` only when asserting writes through a recording `MockDiskIo` (see `pnpm_fix_writes_yaml_to_disk`).\n\n## Common mistakes\n\n| Wrong                                            | Right                                                                                  |\n| ------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| `#[test] fn foo()` + `.run().await`              | `#[tokio::test] async fn foo()`                                                        |\n| `use crate::instance_state::*`                   | `use crate::instance::*`                                                               |\n| `\"pinned\": \"1.0.0\"`                              | `\"pinVersion\": \"1.0.0\"`                                                                |\n| `Context { ... }`                                | `TestBuilder::new()...`                                                                |\n| `ctx.instances[0]`                               | `expect(&ctx).to_have_instances(vec![...])`                                            |\n| `.build()` then check states                     | `.run().await` (or `.build_and_visit_packages()` for sync)                             |\n| Missing `SuspectInstance::*` import              | Import all 4: `FixableInstance::*, ValidInstance::*, SuspectInstance::*, UnfixableInstance::*` |\n\n## Running\n\n```bash\njust test                            # all\ncargo test pinned_test               # pattern match\ncargo test test_name -- --nocapture  # with stdout\n```\n\n## Reference tests\n\n- `src/version_group/banned_test.rs` — banned + custom types\n- `src/version_group/pinned_test.rs` — sub-modules, semver-group interaction\n- `src/version_group/same_range_test.rs` — range satisfaction\n- `src/version_group/catalog_test.rs` — pnpm catalogs\n- `src/version_group/bun_catalog_test.rs` — bun catalogs (sync `.build()`)\n- `src/version_group/preferred_semver_test.rs` — registry updates, update targets\n",".claude/skills/write-tests/patterns.md":"# Test patterns\n\nReal, trimmed examples for the common scenarios. Imports and `mod` wrapping omitted for brevity — see [SKILL.md](SKILL.md) for the full preamble.\n\n## Banned\n\n`src/version_group/banned_test.rs`. Removes a dep; refuses to ban a local-package version.\n\n```rust\n#[tokio::test]\nasync fn refuses_to_ban_local_version() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"version\": \"1.0.0\"}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"package-a\": \"1.1.0\"}}),\n    ])\n    .with_version_group(json!({\n      \"dependencies\": [\"package-a\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::suspect(InvalidLocalVersion),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::suspect(RefuseToBanLocal),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"package-a\",\n      id: \"package-a in /dependencies of package-b\",\n      actual: \"1.1.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Banned with `customTypes`\n\n```rust\n#[tokio::test]\nasync fn removes_instance_with_named_version_string_strategy() {\n  let ctx = TestBuilder::new()\n    .with_config(json!({\n      \"customTypes\": {\n        \"packageManager\": {\n          \"strategy\": \"name@version\",\n          \"path\": \"packageManager\"\n        }\n      }\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"packageManager\": \"pnpm@7.27.0\"\n    })])\n    .with_version_group(json!({\n      \"dependencies\": [\"pnpm\"],\n      \"dependencyTypes\": [\"packageManager\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"pnpm\",\n      id: \"pnpm in /packageManager of package-a\",\n      actual: \"7.27.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Pinned\n\n`src/version_group/pinned_test.rs`.\n\n```rust\n#[tokio::test]\nasync fn an_already_pinned_version_is_valid() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"1.2.0\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIdenticalToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"1.2.0\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Pin overrides a semver-group range\n\n```rust\n#[tokio::test]\nasync fn pin_overrides_semver_group_match() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"^1.0.0\"}\n    }))\n    .with_semver_group(json!({\"range\": \"^\", \"dependencies\": [\"foo\"]}))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.0.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::fixable(PinOverridesSemverRange),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"^1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: Some(\"^1.0.0\"),\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Same range\n\n`src/version_group/same_range_test.rs`. Use a `local`-ignored group as guard so the local instances don't pollute the assertion.\n\n```rust\n#[tokio::test]\nasync fn ranges_satisfy_each_other() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"<=2.0.0\"}}),\n    ])\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-a\",\n      actual: \">=1.0.0\",\n      expected: Some(\">=1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-b\",\n      actual: \"<=2.0.0\",\n      expected: Some(\"<=2.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nFor mismatching ranges the consumer instances become `InstanceState::unfixable(SameRangeMismatch)`.\n\n## Pnpm catalogs\n\n`src/version_group/catalog_test.rs`. `with_pnpm_catalogs` injects `pnpm-workspace.yaml`. Catalog definitions are themselves instances under `/catalog of pnpm-workspace.yaml`.\n\n```rust\n#[tokio::test]\nasync fn catalog_group_definition_valid() {\n  let yaml = \"catalog:\\n  react: ^18.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_version_group(json!({\n      \"label\": \"enforce catalog\",\n      \"dependencies\": [\"react\"],\n      \"policy\": \"catalog\",\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalogDefinition),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^18.0.0\",\n      expected: Some(\"^18.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nNamed catalog: yaml `catalogs:\\n  react18:\\n    react: ^18.0.0\\n`, consumer `\"react\": \"catalog:react18\"`, def id `react in /catalogs/react18 of pnpm-workspace.yaml`.\n\n## Bun catalogs\n\n`src/version_group/bun_catalog_test.rs`. Two shapes:\n\n- `with_bun_catalogs(json!({...}))` puts `/catalog`, `/catalogs/{n}` at the synthetic Bun root.\n- `with_bun_workspaces_catalogs(json!({...}))` nests them under `/workspaces/`.\n\nThese tests often use sync `.build()` — they're checking discovery wiring, not the visit pipeline:\n\n```rust\n#[test]\nfn bun_top_level_catalog_definition_emits_instance() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\"name\": \"pkg-a\", \"version\": \"0.0.0\"})])\n    .build();\n  let react = ctx\n    .instances\n    .iter()\n    .find(|i| i.descriptor.name == \"react\")\n    .expect(\"expected a react bun catalog instance\");\n  assert!(react.is_catalog_instance());\n  assert_eq!(react.descriptor.specifier.get_raw(), \"^18.0.0\");\n}\n```\n\nFull validation pass (consumer + def states):\n\n```rust\n#[test]\nfn bun_catalog_definition_with_consumer_valid() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"}\n    })])\n    .build_and_visit_packages();\n  // assert states via expect(&ctx).to_have_instances(...) as elsewhere\n}\n```\n\n## Semver groups\n\nA semver group rewrites the *expected* range; mismatches are `SemverRangeMismatch` (fixable). Combine with version groups to test interaction:\n\n```rust\n#[tokio::test]\nasync fn semver_group_targets_one_package_only() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"^1.2.3\"}}),\n    ])\n    .with_semver_group(json!({\"packages\": [\"package-b\"], \"range\": \"^\"}))\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  // package-b's `^1.2.3` matches the `^` semver group → no mismatch\n  // assert SatisfiesSameRangeGroup on both consumer instances\n}\n```\n\n## Registry updates\n\n`with_registry_updates` mocks the npm registry and implies subcommand `update`. Optional `with_update_target` bounds it.\n\n```rust\n#[tokio::test]\nasync fn pnpm_catalog_def_marked_outdated_when_registry_has_newer_version() {\n  let yaml = \"catalog:\\n  react: ^17.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_registry_updates(json!({\"react\": [\"17.0.0\", \"18.5.0\"]}))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToNpmRegistry),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^17.0.0\",\n      expected: Some(\"^18.5.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nBound the update target:\n\n```rust\nlet ctx = TestBuilder::new()\n  .with_package(json!({\"name\": \"package-a\", \"dependencies\": {\"wat\": \"1.2.3\"}}))\n  .with_update_target(UpdateTarget::Minor)\n  .with_registry_updates(json!({\"wat\": [\"1.2.3\", \"1.3.4\", \"2.0.0\"]}))\n  .run()\n  .await;\n// wat → 1.3.4 (minor cap), not 2.0.0\n```\n\n`UpdateTarget` import: `use crate::cli::UpdateTarget;`.\n"},"items":[{"name":"syncpack.md","path":".claude/rules/syncpack.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/rules/syncpack.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Syncpack\n\nRust CLI tool for synchronising npm dependency versions across JavaScript monorepos.\n\n## Getting Started\n\n1. Run `just` to see available commands\n2. Read `.notes/` for architecture and mental model\n3. Check `skills/` for task-specific guides:\n\n## Communication Style\n\n- Signal over noise\n- Extremely concise. Sacrifice grammar for brevity\n- Action-oriented: what to DO\n- Grounded in facts: cite code/docs\n- BANNED WORDS: basically, essentially, in order to, comprehensive\n- Always use AskUserQuestion when asking questions\n\n## Pre-Implementation Checklist\n\nBefore large changes, answer YES to all:\n\n- Have I identified ALL decision points?\n- Have I listed trade-offs for each approach?\n- Have I asked user which strategy to use?\n- Am I NOT making architectural assumptions?\n- Am I NOT creating files user didn't request?\n\n## Hard Rules: Ask First\n\nMUST ask when:\n\n- User intent unclear or multiple valid approaches exist\n- Breaking changes or core architecture modifications\n- Creating ANY new files/modules not explicitly requested\n- Large refactors (identify ALL decision points first)\n\nMUST NOT:\n\n- Use banned words\n- Assume user wants plan or summary documentation\n- Refactor without asking\n- Make architectural decisions unilaterally\n\n## Proceed Without Asking\n\n- Pattern clearly exists in codebase\n- Following established convention\n- Non-breaking changes\n- Adding tests, fixing obvious bugs\n\n## Documentation Rules: MANDATORY\n\n**NEVER create new documentation files unless explicitly requested.**\n\nExamples of BANNED actions:\n\n- Creating \"status\" docs, \"validation\" docs, \"current-status\" files\n- Creating \"summary\" docs, \"guide\" docs, \"checklist\" files\n- Splitting existing docs into multiple files\n- Creating \"handoff\" docs, \"next steps\" docs\n\n**ONLY update existing docs:**\n\n- Update the original document in place\n- Add sections/checkboxes to existing files\n- Mark tasks complete where they already exist\n\n**If user has ONE plan file, keep ONE plan file.**\n\nDo NOT proliferate documentation. Update existing files only.\n","isInternal":false,"tokens":447,"sizeBytes":2043},{"name":"adding-command.md","path":".claude/skills/add-feature/adding-command.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/add-feature/adding-command.md","title":"Add-feature Skill","category":"anthropic-skill","format":"markdown","content":"# Example: Adding a New Command\n\n<scenario>\nThis guide walks through adding a new `syncpack check` command that lists outdated dependencies without modifying files.\n</scenario>\n\n<step number=\"1\">\n\n## Step 1: Decide on the Visitor\n\n**Question:** Does this command deal with dependency versions or package.json formatting?\n\n**Answer:** Dependency versions → Use `visit_packages`\n\n</step>\n\n<step number=\"2\">\n\n## Step 2: Add Command Enum Variant\n\n**File:** `src/cli.rs`\n\n```rust\npub enum Subcommand {\n  Lint,\n  Fix,\n  Format,\n  Update,\n  List,\n  Json,\n  Check,  // ← Add this\n}\n```\n\n</step>\n\n<step number=\"3\">\n\n## Step 3: Create Command Implementation\n\n**File:** `src/commands/check.rs`\n\n<implementation>\n\n```rust\nuse crate::{commands::ui, context::Context};\n\n/// List outdated dependencies without modifying files\npub fn run(ctx: Context) -> i32 {\n  let mut has_outdated = false;\n\n  ctx.version_groups.iter().for_each(|group| {\n    let mut has_printed_group = false;\n\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n      let mut has_printed_dependency = false;\n\n      dependency\n        .get_sorted_instances()\n        .filter(|instance| instance.is_outdated())\n        .for_each(|instance| {\n          // Lazy print headers only when we find outdated instances\n          if !has_printed_group {\n            ui::group::print_header(&ctx, group);\n            has_printed_group = true;\n          }\n          if !has_printed_dependency {\n            ui::dependency::print(&ctx, dependency, &group.variant);\n            has_printed_dependency = true;\n          }\n\n          // Print the outdated instance\n          if ctx.config.cli.show_instances {\n            ui::instance::print(&ctx, instance, &group.variant);\n          }\n\n          has_outdated = true;\n        });\n    });\n  });\n\n  if has_outdated {\n    1  // Exit with error code\n  } else {\n    ui::util::print_no_issues_found();\n    0  // Exit successfully\n  }\n}\n```\n\n</implementation>\n\n<pattern_notes>\n\n**Pattern notes:**\n\n- Take ownership of `Context`\n- Return `i32` exit code (0 = success, 1 = failure)\n- Use lazy printing (only print headers when needed)\n- Iterate: version groups → dependencies → instances\n- Filter instances by state (`.filter(|instance| instance.is_outdated())`)\n- Use existing UI utilities for consistent output\n\n</pattern_notes>\n\n</step>\n\n<step number=\"4\">\n\n## Step 4: Register Command in Module\n\n**File:** `src/commands.rs`\n\n```rust\npub mod fix;\npub mod format;\npub mod json;\npub mod lint;\npub mod list;\npub mod update;\npub mod check;  // ← Add this\n```\n\n</step>\n\n<step number=\"5\">\n\n## Step 5: Add to Main Dispatch\n\n**File:** `src/main.rs`\n\n```rust\nuse {\n  crate::{\n    commands::{check, fix, format, json, lint, list, update},  // ← Add check\n    // ... other imports\n  },\n};\n\n// ... in main() function:\n\nlet _exit_code = match ctx.config.cli.subcommand {\n  Subcommand::Fix => {\n    let ctx = visit_packages(ctx);\n    fix::run(ctx)\n  }\n  Subcommand::Format => {\n    let ctx = visit_formatting(ctx);\n    format::run(ctx)\n  }\n  Subcommand::Lint => {\n    let ctx = visit_packages(ctx);\n    lint::run(ctx)\n  }\n  Subcommand::Update => {\n    let mut ctx = ctx;\n    ctx.fetch_all_updates().await;\n    let ctx = visit_packages(ctx);\n    update::run(ctx)\n  }\n  Subcommand::List => {\n    let ctx = visit_packages(ctx);\n    list::run(ctx)\n  }\n  Subcommand::Json => {\n    let ctx = visit_packages(ctx);\n    json::run(ctx)\n  }\n  Subcommand::Check => {  // ← Add this\n    let ctx = visit_packages(ctx);\n    check::run(ctx)\n  }\n};\n```\n\n<key_points>\n\n**Key points:**\n\n- Must call `visit_packages(ctx)` before command (to assign InstanceStates)\n- Pass the returned Context to your command's run function\n- The command consumes Context and returns exit code\n\n</key_points>\n\n</step>\n\n<step number=\"6\">\n\n## Step 6: Add CLI Help Text (Optional)\n\n**File:** `src/cli.rs`\n\n```rust\n#[derive(Debug, clap::Parser)]\n#[command(name = \"syncpack\")]\n#[command(about = \"Manage multiple package.json files\")]\npub struct Cli {\n  #[command(subcommand)]\n  pub subcommand: Subcommand,\n  // ... other fields\n}\n\n#[derive(Debug, clap::Subcommand)]\npub enum Subcommand {\n  /// Find and report outdated dependencies\n  #[command(name = \"check\")]\n  Check,\n  // ... other variants\n}\n```\n\n</step>\n\n<step number=\"7\">\n\n## Step 7: Write Tests\n\n**File:** `src/commands/check_test.rs` (or add to existing test file)\n\n<test_examples>\n\n```rust\nuse {\n  crate::{\n    commands::check,\n    instance_state::{FixableInstance::*, InstanceState, ValidInstance::*},\n    test::{builder::TestBuilder, expect::ExpectedInstance},\n  },\n  serde_json::json,\n};\n\n#[test]\nfn reports_outdated_dependencies() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"17.0.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"17.0.0\", \"17.0.1\", \"18.0.0\", \"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  // Verify the instance is marked as outdated\n  let react_instance = ctx.instances.iter()\n    .find(|i| i.dependency.name == \"react\")\n    .expect(\"Should find react instance\");\n\n  assert!(react_instance.is_outdated());\n\n  // Run the command (in real test, you'd capture stdout)\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 1, \"Should exit with error when outdated found\");\n}\n\n#[test]\nfn exits_successfully_when_up_to_date() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"18.2.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 0, \"Should exit successfully when up to date\");\n}\n```\n\n</test_examples>\n\n</step>\n\n<step number=\"8\">\n\n## Step 8: Test Locally\n\n```bash\n# Build and run\ncargo run -- check --help\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- check\n\n# Run tests\ncargo test check\njust test\n```\n\n</step>\n\n<step number=\"9\">\n\n## Step 9: Verify All Places Updated\n\n<checklist>\n\n- [x] Added to `Subcommand` enum in `src/cli.rs`\n- [x] Created `src/commands/check.rs` with `pub fn run(ctx: Context) -> i32`\n- [x] Registered in `src/commands.rs` module\n- [x] Added match arm in `src/main.rs`\n- [x] Called appropriate visitor (`visit_packages`)\n- [x] Written tests\n- [x] Tested locally\n\n</checklist>\n\n</step>\n\n<variations>\n\n## Common Variations\n\n<variation type=\"file_modification\">\n\n### Command that modifies files\n\n```rust\npub fn run(mut ctx: Context) -> i32 {\n  // ... iterate and modify instances\n\n  // Write changes to disk\n  ctx.packages.write_all();\n\n  0\n}\n```\n\n</variation>\n\n<variation type=\"registry\">\n\n### Command that needs registry data\n\nIn `main.rs`:\n\n```rust\nSubcommand::MyCommand => {\n  let mut ctx = ctx;\n  ctx.fetch_all_updates().await;  // Fetch from npm\n  let ctx = visit_packages(ctx);\n  my_command::run(ctx)\n}\n```\n\n</variation>\n\n<variation type=\"cli_options\">\n\n### Command with specific CLI options\n\n```rust\npub enum Subcommand {\n  #[command(name = \"check\")]\n  Check {\n    /// Only check production dependencies\n    #[arg(long)]\n    prod_only: bool,\n  },\n}\n```\n\nAccess in command:\n\n```rust\nif let Subcommand::Check { prod_only } = &ctx.config.cli.subcommand {\n  if *prod_only {\n    // Filter logic\n  }\n}\n```\n\n</variation>\n\n</variations>\n\n<troubleshooting>\n\n## Troubleshooting\n\n<problem>\n**Problem:** Command not found\n→ Check you added it to all three places: enum, main.rs match, commands module\n</problem>\n\n<problem>\n**Problem:** Instances have `Unknown` state\n→ You forgot to call `visit_packages(ctx)` before your command\n</problem>\n\n<problem>\n**Problem:** Compilation error about moved value\n→ Make sure your command signature is `pub fn run(ctx: Context) -> i32`\n</problem>\n\n<problem>\n**Problem:** Changes not persisting\n→ You need to call `ctx.packages.write_all()` in commands that modify files\n</problem>\n\n</troubleshooting>\n\n<related_examples>\n\n## Related Examples\n\n- See `src/commands/lint.rs` for read-only command\n- See `src/commands/fix.rs` for file-modifying command\n- See `src/commands/update.rs` for command using registry client\n\n</related_examples>\n","isInternal":false,"tokens":2113,"sizeBytes":8244},{"name":"adding-instance-state.md","path":".claude/skills/add-feature/adding-instance-state.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/add-feature/adding-instance-state.md","title":"Add-feature Skill","category":"anthropic-skill","format":"markdown","content":"# Adding a New InstanceState Variant\n\n<scenario>\nThis guide walks through adding new `InstanceState` variants. The example used\nthroughout is `SameMinorHasMajorMismatch`, a real variant added during the\nsameMinor improvement work. It is an `Unfixable` state assigned when instances\nin a `sameMinor` version group have different MAJOR versions.\n</scenario>\n\n---\n\n## Step 1: Choose the right category\n\n```\nInstanceState\n├── Unknown              — never assign this; it is the initial pre-inspection state\n├── Valid(ValidInstance) — instance follows all rules correctly\n├── Invalid(InvalidInstance)\n│   ├── Fixable(FixableInstance)                  — syncpack knows the correct value and can auto-fix\n│   ├── Unfixable(UnfixableInstance)              — ambiguous; needs human decision\n│   └── Conflict(SemverGroupAndVersionConflict)   — two active rules are simultaneously unsatisfiable\n└── Suspect(SuspectInstance)                      — user misconfiguration\n```\n\nAsk:\n\n- Do we know the correct value? → `Fixable`\n- Is the situation ambiguous, multiple valid options? → `Unfixable`\n- Are two rules active and irreconcilable? → `Conflict`\n- Has the user misconfigured something? → `Suspect`\n- Is everything fine? → `Valid`\n\nFor `SameMinorHasMajorMismatch`: crossing a major version boundary is unsafe\nand we cannot pick a winner without asking the user → `Unfixable`.\n\n---\n\n## Step 2: Add the variant to `src/instance_state.rs`\n\nFind the appropriate enum and add the variant with a multi-line doc comment.\nEvery condition relevant to the state must be listed. Use these symbols:\n\n- `✓` — condition is true / requirement is met\n- `✘` — condition is false / requirement is violated\n- `?` — ambiguous / unknown / why we cannot auto-fix\n- `!` — notable side effect or policy decision\n\n```rust\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum UnfixableInstance {\n  // ... existing variants ...\n\n  /// - ✘ Instance is in a sameMinor version group\n  /// - ✘ One or more other instances have a different MAJOR version\n  /// - ? Crossing a major version boundary is unsafe\n  /// - ? We cannot know which MAJOR the user wants and have to ask them\n  SameMinorHasMajorMismatch,\n}\n```\n\nThe doc comment is the source of truth for when this state is assigned. Be\nprecise — every bullet should be independently verifiable from the visitor code.\n\n---\n\n## Step 3: Check whether any `impl` blocks need updating\n\nMost additions require no changes to `impl InstanceState`. The severity, the\n`is_fixable()` / `is_unfixable()` helpers, and the `get_name()` / `get_status_type()`\nmethods all work by matching on the outer category (`Valid`, `Invalid`,\n`Suspect`), not on individual variants.\n\nThe only time you need to touch `impl InstanceState` is if you are adding a\nvariant that needs a **custom severity** (rare) or a new top-level predicate\n(e.g. `is_banned()`). For ordinary additions, no changes are needed.\n\n---\n\n## Step 4: Write the detection logic in the visitor\n\nVisitor files live in `src/visit_packages/`. Each visitor receives a\n`&Dependency` and must call one of the mark methods on every instance:\n\n```rust\ninstance.mark_valid(ValidInstance::SatisfiesSameMinorGroup, specifier);\ninstance.mark_fixable(FixableInstance::DiffersToPin, expected_specifier);\ninstance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\ninstance.mark_conflict(SemverGroupAndVersionConflict::MatchConflictsWithLocal);\ninstance.mark_suspect(SuspectInstance::InvalidLocalVersion);\n```\n\nNever assign to `instance.state` directly — always use these methods.\n\nThe specifier argument to `mark_valid` and `mark_fixable` is the **expected**\nspecifier — what the instance should be set to. Read the actual specifier from\n`instance.descriptor.specifier`.\n\nExample — marking all instances when a major mismatch is detected (computed\nonce before the per-instance loop):\n\n```rust\nlet has_major_mismatch = dependency\n    .instances\n    .iter()\n    .any(|i| !i.already_has_same_major_as_all(&dependency.instances));\n\ndependency.instances.iter().for_each(|instance| {\n    let actual_specifier = &instance.descriptor.specifier;\n\n    if has_major_mismatch {\n        instance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\n        return;\n    }\n\n    // ... rest of visitor logic ...\n});\n```\n\nKey rules:\n\n- **Every instance must be marked** before the visitor returns. No instance may\n  remain in `InstanceState::Unknown` after `visit_packages()` completes.\n- **Compute per-dependency values once** before the loop (e.g. highest\n  specifier, major-mismatch boolean) to avoid O(n²) re-checking.\n- **Do not read `instance.state` inside the visitor** to decide what to do —\n  states are write-once during inspection.\n\n---\n\n## Step 5: Integrate into `visit_packages.rs` (only for new visitors)\n\nIf you are adding a variant to an **existing** visitor, skip this step.\n\nIf you are adding a **new visitor module**, register it in\n`src/visit_packages.rs`:\n\n```rust\nmod banned;\nmod ignored;\nmod indent;\nmod pinned;\nmod preferred_semver;\nmod same_minor;   // ← example of an existing entry\nmod same_range;\nmod snapped_to;\nmod your_new_visitor;  // ← add here\n\npub fn visit_packages(ctx: Context) -> Context {\n    ctx.version_groups.iter().sorted_by(order_snapped_to_groups_last).for_each(|group| {\n        group.dependencies.values().for_each(|dependency| match dependency.variant {\n            VersionGroupVariant::Banned => banned::visit(dependency),\n            VersionGroupVariant::SameMinor => same_minor::visit(dependency),\n            // ... add your new variant arm here if a new VersionGroupVariant was added ...\n            VersionGroupVariant::SnappedTo => snapped_to::visit(dependency, &ctx),\n        });\n    });\n    ctx\n}\n```\n\n---\n\n## Step 6: Write tests\n\nTest files live alongside their visitor: `src/visit_packages/same_minor_test.rs`\nis declared inside `same_minor.rs` with:\n\n```rust\n#[cfg(test)]\n#[path = \"same_minor_test.rs\"]\nmod same_minor_test;\n```\n\nTests use `TestBuilder` and `expect(&ctx).to_have_instances(...)`. Every\n`ExpectedInstance` must specify:\n\n- `state` — the exact `InstanceState` expected\n- `dependency_name` — the dependency name (not the instance id)\n- `id` — `\"{dep} in {location} of {package}\"` format\n- `actual` — the on-disk specifier string\n- `expected` — `Some(\"...\")` for the expected/fix-target specifier, or `None`\n- `overridden` — `Some(\"...\")` when a semver group preference was overridden\n\n```rust\nuse {\n    crate::{\n        instance_state::{FixableInstance::*, InstanceState, UnfixableInstance::*, ValidInstance::*},\n        test::{\n            builder::TestBuilder,\n            expect::{expect, ExpectedInstance},\n        },\n    },\n    serde_json::json,\n};\n\n#[test]\nfn major_mismatch_marks_all_instances_unfixable() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![json!({\n            \"name\": \"my-project\",\n            \"version\": \"1.0.0\",\n            \"dependencies\": {\n                \"foo\": \"1.2.0\"\n            },\n            \"devDependencies\": {\n                \"foo\": \"2.1.0\"\n            }\n        })])\n        .with_version_groups(vec![json!({\n            \"dependencies\": [\"foo\"],\n            \"policy\": \"sameMinor\"\n        })])\n        .build_and_visit_packages();\n\n    expect(&ctx).to_have_instances(vec![\n        ExpectedInstance {\n            state: InstanceState::valid(IsLocalAndValid),\n            dependency_name: \"my-project\",\n            id: \"my-project in /version of my-project\",\n            actual: \"1.0.0\",\n            expected: Some(\"1.0.0\"),\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /dependencies of my-project\",\n            actual: \"1.2.0\",\n            expected: None,\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /devDependencies of my-project\",\n            actual: \"2.1.0\",\n            expected: None,\n            overridden: None,\n        },\n    ]);\n}\n```\n\nCover every leaf of the decision tree. For a visitor with semver group\ninteraction, the minimum test matrix is:\n\n| scenario                             | test name pattern                                          |\n| ------------------------------------ | ---------------------------------------------------------- |\n| no semver group, correct version     | `satisfies_..._and_has_no_semver_group`                    |\n| no semver group, wrong version       | `has_..._mismatch_and_no_semver_group`                     |\n| safe semver group, already matches   | `satisfies_..._and_matches_compatible_..._semver_group`    |\n| safe semver group, does not match    | `satisfies_..._but_mismatches_compatible_..._semver_group` |\n| unsafe semver group, already matches | `..._but_range_matches_incompatible_..._semver_group`      |\n| unsafe semver group, does not match  | `..._but_range_mismatches_incompatible_..._semver_group`   |\n\n---\n\n## Step 7: Verify commands need no changes\n\nCommands (`src/commands/*.rs`) operate on states generically:\n\n```rust\n// lint.rs, fix.rs etc. already handle your new variant automatically\ndependency.get_sorted_instances()\n    .filter(|instance| instance.is_invalid())  // or is_fixable(), is_unfixable() etc.\n    .for_each(|instance| { ... });\n```\n\nNew variants within existing categories (`Fixable`, `Unfixable`, etc.) are\npicked up automatically. You only need to touch command files if your new state\nrequires **bespoke fix logic** — for example, `IsBanned` requires removing the\ndependency entry rather than updating a version string.\n\n---\n\n## Step 8: Run the tests\n\n```bash\n# Run tests for a specific visitor\ncargo test same_minor\n\n# Run all tests\njust test\n```\n\n---\n\n## Checklist\n\n- [ ] Variant added to the correct enum in `src/instance_state.rs`\n- [ ] Doc comment uses `✓` `✘` `?` `!` bullets and describes every condition\n- [ ] Visitor uses `instance.mark_*()` methods — no direct state assignment\n- [ ] Per-dependency values computed once before the per-instance loop\n- [ ] Every instance is marked on every code path through the visitor\n- [ ] Tests cover every leaf of the decision tree\n- [ ] Tests use `expect(&ctx).to_have_instances(...)` with `ExpectedInstance`\n- [ ] No command changes needed (or bespoke fix logic added if required)\n\n---\n\n## Related files\n\n- `src/instance_state.rs` — all state variant definitions and annotations\n- `src/visit_packages.rs` — visitor dispatch\n- `src/visit_packages/*.rs` — visitor implementations\n- `src/visit_packages/*_test.rs` — test examples to copy patterns from\n- `src/commands/fix.rs` — example of how states drive fix behaviour\n- `.notes/reference/version-group-implementation-rules.md` — cross-cutting rules for all visitors\n","isInternal":false,"tokens":2551,"sizeBytes":10975},{"name":"SKILL.md","path":".claude/skills/add-feature/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/add-feature/SKILL.md","title":"add-feature","category":"anthropic-skill","format":"markdown","content":"---\nname: add-feature\ndescription: Add new features to Syncpack including commands and validation logic. Use when implementing new CLI commands, adding InstanceState variants, or extending version group behaviour.\n---\n\n# Add Feature\n\nGuide for adding new functionality to Syncpack.\n\n## Quick Decision\n\nWhat are you adding?\n\n- **New CLI command** (lint, fix, format, etc.) → See [adding-command.md](adding-command.md)\n- **New validation logic** (InstanceState variant) → See [adding-instance-state.md](adding-instance-state.md)\n- **Both** → Start with instance state, then add command\n\n## Prerequisites\n\nBefore adding any feature:\n\n1. Understand the 3-phase pattern: Create Context → Inspect Context → Run Command\n2. Know which phase your feature affects\n3. Check if similar code exists (use `ast-grep -p 'PATTERN' src/`)\n\n## Feature Types\n\n### Commands\n\nCommands are user-facing operations (`syncpack lint`, `syncpack fix`, etc.).\n\n**When to create a new command:**\n\n- New user-facing operation that doesn't fit existing commands\n- Different output format or behaviour needed\n\n**Registration points (all three required):**\n\n1. `src/cli.rs` - Add to `Subcommand` enum\n2. `src/main.rs` - Add match arm for dispatch\n3. `src/commands/*.rs` - Implement `pub fn run(ctx: Context) -> i32`\n\n→ Full guide: [adding-command.md](adding-command.md)\n\n### Instance States\n\nInstanceState variants describe validation results (valid, fixable, unfixable, suspect).\n\n**When to add a new state:**\n\n- New validation rule needed\n- New type of error/warning to report\n- New auto-fix capability\n\n**Location:** `src/instance_state.rs` + `src/visit_packages/*.rs`\n\n→ Full guide: [adding-instance-state.md](adding-instance-state.md)\n\n## Common Workflow\n\n1. Write failing test using TestBuilder\n2. Implement minimal code to pass\n3. Run `cargo clippy` and fix warnings\n4. Refactor if needed\n5. Update documentation\n\n## Checklist\n\nBefore submitting:\n\n- [ ] Tests pass (`just test`)\n- [ ] Zero clippy warnings\n- [ ] Follows existing patterns\n- [ ] Registered in all required places (for commands)\n- [ ] Uses TDD approach\n","frontmatter":{"name":"add-feature","description":"Add new features to Syncpack including commands and validation logic. Use when implementing new CLI commands, adding InstanceState variants, or extending version group behaviour."},"isInternal":false,"tokens":494,"sizeBytes":2104},{"name":"SKILL.md","path":".claude/skills/document-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/document-code/SKILL.md","title":"document-code","category":"anthropic-skill","format":"markdown","content":"---\nname: document-code\ndescription: Add and update the documentation website for Syncpack. Use when making user-facing changes to the codebase.\n---\n\n# Document Code\n\nGuide for adding and updating Syncpack's documentation website.\n\n## Quick Decision\n\nWhat are you documenting?\n\n| Adding/Updating...    | Location                                         |\n| --------------------- | ------------------------------------------------ |\n| Command docs          | `site/src/content/docs/command/[cmd].mdx`        |\n| Config property       | `site/src/content/docs/config/[prop].mdx`        |\n| Version group variant | `site/src/content/docs/version-groups/[var].mdx` |\n| Semver group variant  | `site/src/content/docs/semver-groups/[var].mdx`  |\n| Status code           | `site/src/content/docs/status/[code].mdx`        |\n| FAQ                   | `site/src/faq/[id].mdx`                          |\n| Shared option partial | `site/src/_partials/option/[opt].mdx`            |\n| Reusable component    | `site/src/components/[name].astro`               |\n\n## Workflow\n\n1. **Read source** — `cli.rs` for commands, `rcfile.rs` for config, `instance_state.rs` for status codes\n2. **Create/update MDX** — Match existing structure exactly\n3. **Add link identifier** — Update `globalReferenceLinks` in `astro.config.mjs`\n4. **Test locally** — `pnpm run dev` from `site/`\n5. **Verify links** — Check all `(IDENTIFIER)` links resolve\n\n## Source to Docs Mapping\n\n| Source                  | Documentation                                         |\n| ----------------------- | ----------------------------------------------------- |\n| `src/cli.rs` Subcommand | `site/src/content/docs/command/*.mdx`                 |\n| `src/rcfile.rs` Rcfile  | `site/src/content/docs/config/*.mdx`                  |\n| `src/instance_state.rs` | `site/src/content/docs/status/*.mdx`                  |\n| `src/specifier.rs`      | `site/src/content/docs/reference/specifier-types.mdx` |\n\n## Local Development\n\n```bash\ncd site\npnpm run dev          # Start at http://localhost:4321/syncpack\npnpm run build        # Verify build succeeds\n```\n\n## Site Overview\n\n- Source located at `site/src/`\n- Built with [Starlight](https://starlight.astro.build/) on top of [Astro](https://astro.build/)\n- Content at `site/src/content/docs/**/*.mdx`\n- Reusable partials at `site/src/_partials/**/*.mdx`\n- Reusable components at `site/src/components/*.astro`\n\n## Command Documentation\n\nEach command has a corresponding file at `site/src/content/docs/command/[command].mdx`.\n\n### Required Structure (in order)\n\n1. Description of the command's purpose from end user perspective\n2. `## Examples` — Single bash code block with commented examples\n3. `## Options` — Start with `<QuoteFilters />` callout, then `###` for each option alphabetically (`--help` last)\n\n### Shared Option Partials\n\nMany commands share options. Define reusable partials at `site/src/_partials/option/*.mdx`:\n\n```mdx\nimport ConfigOption from \"@partials/option/config.mdx\";\n\n<ConfigOption command=\"update\" />\n```\n\n## Config Documentation\n\nEach config property has a file at `site/src/content/docs/config/[property].mdx`.\n\n### Required Structure\n\n1. Description of the config's purpose from end user perspective\n2. `## Default Value` — Code block showing default (when applicable)\n3. `## Examples` — Code blocks with commented examples\n\n### Version Groups and Semver Groups\n\nEach variant has its own page:\n\n- `site/src/content/docs/version-groups/*.mdx`\n- `site/src/content/docs/semver-groups/*.mdx`\n\n#### Required Structure\n\n1. Description of the group's purpose from end user perspective\n2. `## Examples` — Code block per example, capture novel use cases\n3. `## Configuration` — `###` for each config property\n\n## Status Code Documentation\n\nEach status code has a file at `site/src/content/docs/status/[code].mdx`.\n\nReference page at `site/src/content/docs/reference/status-codes.mdx` explains the `InstanceState` enum and its nested enums (`ValidInstance`, `InvalidInstance`, `SuspectInstance`).\n\n## FAQ Documentation\n\n- Create FAQ files at `site/src/faq/[id].mdx`\n- Listed at `/syncpack/faq` via `site/src/pages/faq.astro`\n- Individual pages via `site/src/pages/faq/[id].astro`\n\n## Linking Between Pages\n\nUse named identifiers in markdown links:\n\n```mdx\nWhen using the [update](COMMAND_UPDATE) command\n```\n\nDefine identifiers in `globalReferenceLinks` function in `site/astro.config.mjs`.\n\n## Open Graph Images\n\nGenerated by `site/src/pages/og/[...path].png.ts` using [Satori](https://github.com/vercel/satori).\n\n## Troubleshooting\n\n| Problem                       | Fix                                           |\n| ----------------------------- | --------------------------------------------- |\n| Link not working              | Check identifier in `globalReferenceLinks`    |\n| Partial not rendering         | Verify import path uses `@partials/` alias    |\n| OG image broken               | Check `og/[...path].png.ts` handles new route |\n| Build fails                   | Run `pnpm run build` locally to see error     |\n| Page not appearing in sidebar | Check frontmatter and Starlight config        |\n\n## Checklist\n\nBefore submitting documentation:\n\n- [ ] Structure matches existing pages of same type\n- [ ] Link identifier added to `astro.config.mjs`\n- [ ] Local dev server renders correctly\n- [ ] Build succeeds (`pnpm run build`)\n- [ ] All internal links resolve\n","frontmatter":{"name":"document-code","description":"Add and update the documentation website for Syncpack. Use when making user-facing changes to the codebase."},"isInternal":false,"tokens":1222,"sizeBytes":5412},{"name":"SKILL.md","path":".claude/skills/fix-bug/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/fix-bug/SKILL.md","title":"fix-bug","category":"anthropic-skill","format":"markdown","content":"---\nname: fix-bug\ndescription: Debug and fix bugs in Syncpack using scientific debugging methodology. Use when a test is failing, unexpected behaviour occurs, or investigating issues. Covers hypothesis-driven debugging and TDD-based fixes.\n---\n\n# Fix Bug\n\nGuide for debugging and fixing bugs in Syncpack.\n\n## Scientific Debugging Workflow\n\n1. **Observe** — Understand the symptom precisely\n2. **Hypothesise** — Form a theory about the root cause\n3. **Experiment** — Test the hypothesis with targeted changes\n4. **Validate** — Confirm the fix with tests\n5. **Verify** — Run full test suite\n\n## Step 1: Observe\n\nGather information before changing code:\n\n```bash\n# Run the failing test with output\ncargo test test_name -- --nocapture\n\n# Run with backtrace\nRUST_BACKTRACE=1 cargo test test_name\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- lint\n```\n\nQuestions to answer:\n\n- What is the exact error message?\n- What input triggers it?\n- What is the expected vs actual behaviour?\n\n## Step 2: Hypothesise\n\nCommon root causes by symptom:\n\n| Symptom              | Likely Cause                                            |\n| -------------------- | ------------------------------------------------------- |\n| State is `Unknown`   | `visit_packages` not called or instance skipped         |\n| Wrong state assigned | Validation logic order, earlier check overriding        |\n| Instance not found   | Package/dependency name mismatch, location format       |\n| Command not running  | Missing registration in cli.rs, main.rs, or commands.rs |\n| Panic/unwrap failure | Unexpected None or Err value                            |\n\n## Step 3: Experiment\n\nAdd targeted debug output:\n\n```rust\nuse log::debug;\n\ndebug!(\"Instance: {:#?}\", instance);\ndebug!(\"State before: {:?}\", instance.state.borrow());\n```\n\nSearch for related code:\n\n```bash\n# Find where state is assigned\nast-grep -p 'InstanceState::fixable' src/visit_packages/\n\n# Find similar patterns\nast-grep -p 'PATTERN' src/\n```\n\n## Step 4: Validate\n\nWrite a failing test FIRST (TDD):\n\n```rust\n#[test]\nfn reproduces_the_bug() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![/* minimal reproduction */])\n        .build_and_visit_packages();\n\n    // Assert expected behaviour (will fail initially)\n}\n```\n\nThen fix the code until the test passes.\n\n## Step 5: Verify\n\n```bash\njust test        # All tests pass\ncargo clippy     # No warnings\n```\n\n## Where to Look\n\n### By component\n\n| Issue with...    | Check...                                 |\n| ---------------- | ---------------------------------------- |\n| CLI parsing      | `src/cli.rs`                             |\n| Config loading   | `src/config.rs`, `src/rcfile.rs`         |\n| Package reading  | `src/packages.rs`, `src/package_json.rs` |\n| State assignment | `src/visit_packages/*.rs`                |\n| Command output   | `src/commands/*.rs`, `src/commands/ui/`  |\n| Version parsing  | `src/specifier/*.rs`                     |\n\n### By phase\n\n| Phase   | What happens                             | Files                                      |\n| ------- | ---------------------------------------- | ------------------------------------------ |\n| Create  | Read config, packages, collect instances | `context.rs`, `packages.rs`                |\n| Inspect | Assign InstanceState                     | `visit_packages.rs`, `visit_packages/*.rs` |\n| Run     | Process instances, output/write          | `commands/*.rs`                            |\n\n## Common Fixes\n\n### Instance state not being assigned\n\nCheck:\n\n1. Is the instance's version group variant handled in `visit_packages.rs`?\n2. Is there an early return skipping this instance?\n3. Is another validation assigning a state first?\n\n### Test assertion failing\n\nCheck:\n\n1. Is `build_and_visit_packages()` called (not just `build()`)?\n2. Is the location string format correct? (`\"{dep} in {location} of {package}\"`)\n3. Are there multiple instances of the same dependency?\n\n### Command not found\n\nVerify registration in all three places:\n\n1. `src/cli.rs` — `Subcommand` enum\n2. `src/main.rs` — match arm\n3. `src/commands.rs` — module declaration\n\n## Debugging Tips\n\n- **Isolate** — Create minimal reproduction\n- **Binary search** — Comment out code to narrow down\n- **Print early** — Add debug output before suspected area\n- **Check order** — Validation order matters (first match wins)\n- **Read tests** — Existing tests show expected behaviour\n\n## Anti-Patterns\n\n❌ Changing code without understanding the cause\n❌ Removing code just to make tests pass\n❌ Fixing symptoms instead of root cause\n❌ Skipping the test-first step\n","frontmatter":{"name":"fix-bug","description":"Debug and fix bugs in Syncpack using scientific debugging methodology. Use when a test is failing, unexpected behaviour occurs, or investigating issues. Covers hypothesis-driven debugging and TDD-based fixes."},"isInternal":false,"tokens":1011,"sizeBytes":4638},{"name":"SKILL.md","path":".claude/skills/front-loading/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/front-loading/SKILL.md","title":"front-loading","category":"anthropic-skill","format":"markdown","content":"---\nname: front-loading\ndescription: Put the answer first, then context and details follow. Use when writing documentation, tutorials, error messages, or any communication where readers need to decide quickly if something is relevant to them.\n---\n\n# Front-Loading\n\nPut the answer first. Context follows.\n\n## What This Is\n\nFront-loading means leading with the most important information. Users decide in the first few words whether to keep reading.\n\nStructure your communication as:\n\n1. **The point** (first sentence)\n2. **Why it matters** (next 1–2 sentences)\n3. **Details and evidence** (rest of content)\n\nMost writers do the opposite: context first, then conclusion. Readers get lost in the journey before reaching the destination.\n\n## When to Use\n\n- Writing documentation or guides\n- Structuring arguments or explanations\n- Creating tutorial sections\n- Writing error messages\n- Composing email or feedback\n- Organizing any sequential information\n\nFront-load at every level: sentences, paragraphs, sections, documents.\n\n## How to Apply\n\n### 1. Lead with the Conclusion\n\nPut the answer first. Don't make readers wait.\n\n**Bad (context first):**\n\"There are many different ways to approach testing. Some teams test before writing code. Others test after. Some tests run quickly, others take longer. The most important thing is consistency. So you should write tests.\"\n\n**Good (conclusion first):**\n\"Write tests. Consistency matters more than approach.\"\n\nThen explain the trade-offs if needed.\n\n### 2. Answer the Reader's Question Immediately\n\nReaders have a question in mind. Answer it in the first sentence.\n\n**Bad (makes reader wait):**\n\"Syncpack is a CLI tool built in Rust. It uses a configuration file in JSON format. The configuration file specifies rules for how to synchronise dependencies. These rules can be customized per workspace...\"\n\nReader's question: \"How do I configure Syncpack?\" Buried in paragraph 3.\n\n**Good (answers immediately):**\n\"Configure Syncpack in `.syncpackrc.json` at your monorepo root. Specify version groups and rules for each.\"\n\nThen explain each option.\n\n### 3. Use Signposting\n\nTell readers what's coming before they read it.\n\n**Bad (no preview):**\n\"First, install Node 18. Then, verify the installation. Next, clone the repository. After that, install dependencies...\"\n\nReader: \"Where is this going?\"\n\n**Good (preview structure):**\n\"Three steps: Install Node, clone the repo, and run `npm install`.\"\n\nThen detail each step.\n\n### 4. When Explaining a Process, Show the End State First\n\nLet readers know what success looks like before they start.\n\n**Bad (journey without destination):**\n\"Follow these 12 steps to set up CI/CD. Step 1: Create a workflow file. Step 2: Add a trigger. Step 3: Define a job...\"\n\nReader: \"Why am I doing this?\"\n\n**Good (end state first):**\n\"Goal: Your PR automatically runs tests and fails if any break. Here's how to set it up in 12 steps.\"\n\nThen detail steps 1–12.\n\n### 5. Structure Paragraphs with Topic Sentence First\n\nEach paragraph's first sentence should be its main idea.\n\n**Bad (idea buried):**\n\"There are many different caching strategies. Some store values in memory. Others use disk. Still others use remote services. The strategy you choose depends on your use case. For most applications, in-memory caching is sufficient.\"\n\nReader has to parse the whole paragraph to find the point.\n\n**Good (topic first):**\n\"For most applications, in-memory caching is sufficient. Other strategies (disk, remote) exist for specific cases. Choose based on your performance and persistence needs.\"\n\n### 6. When Providing Options, Lead with the Recommendation\n\nDon't make readers compare three options to find the best one.\n\n**Bad (no guidance):**\n\"You can use approach A, B, or C. Each has trade-offs. A is simple but inflexible. B is flexible but complex. C is somewhere in between. Most projects use B or C.\"\n\nReader: \"So which should I use?\"\n\n**Good (recommendation first):**\n\"Use approach B (flexible but complex). It scales as your needs grow. Use approach A only if simplicity is critical. Approach C rarely adds value.\"\n\n## Examples\n\n### Documentation Section\n\n**Bad (context first):**\n\"The registry client handles communication with npm. Over time, performance became an issue. We realized the client was making unnecessary requests. After analysis, we found that caching solved the problem. So the registry client now caches responses.\"\n\n**Good (point first):**\n\"The registry client caches npm responses to improve performance. This reduced request overhead by 60%.\"\n\nThen explain the details.\n\n---\n\n### Error Message\n\n**Bad (generic context):**\n\"An operation has encountered an error condition. The system attempted to perform a task. This task requires certain prerequisites. One or more prerequisites were not met. Please verify your setup.\"\n\n**Good (specific answer):**\n\"Missing required field in config: 'versionGroups'. See `.syncpackrc.json` example.\"\n\n---\n\n### Tutorial Section\n\n**Bad (setup before goal):**\n\"Before you can run Syncpack, you need to set up your monorepo structure. This involves creating a root directory. Inside this directory, you need workspace packages. Each package should have a `package.json`. Once you have this structure, you can run Syncpack.\"\n\n**Good (goal first):**\n\"Goal: Run Syncpack to sync versions across your monorepo.\"\n\nThen list the prerequisites (monorepo structure, workspace packages).\n\n---\n\n### Workflow Documentation\n\n**Bad (steps without outcome):**\n\"Step 1: Write your implementation. Step 2: Write tests. Step 3: Run tests locally. Step 4: Push to GitHub. Step 5: Wait for CI. Step 6: Address failures. Step 7: Merge when green.\"\n\n**Good (outcome first):**\n\"Goal: Merge code only when tests pass and CI approves.\"\n\nThen list the steps to reach that goal.\n\n## Trade-offs\n\n### When NOT to front-load\n\n- **Narrative/storytelling:** Sometimes the journey is the point. Build suspense if appropriate.\n- **Teaching from first principles:** Some explanation is necessary before jumping to conclusions.\n- **Building trust:** Sometimes context _is_ the point (e.g., \"here's why we made this decision\").\n- **Sensitive feedback:** Sometimes leading with criticism is harmful. Lead with understanding instead.\n\nThe principle: **Match the reader's mental model.** If they need context to understand the conclusion, provide context first.\n\n### Common mistake\n\nOver-front-loading: \"Use this. End of story.\" without any reasoning alienates readers who need understanding.\n\nSolve by asking: \"Will the reader understand why this conclusion is right?\" If no, provide enough context. But lead with the conclusion, not the context.\n\n## Testing\n\nHow to know if you've front-loaded effectively:\n\n1. **First sentence test:** Can someone read only the first sentence and understand the main point?\n2. **Skim test:** Someone skims headings and bullets—do they get the gist?\n3. **Reader expectation test:** \"What happens next?\" readers predict after each section. Were they right?\n4. **Reduction test:** Remove everything after the first paragraph. Is the main point clear? (If yes, you front-loaded successfully.)\n5. **Fresh eyes test:** Have someone read the first paragraph and summarize. Did they get it right?\n\n## Application in Syncpack\n\nFront-loading improves:\n\n- **Skill descriptions:** Users know instantly if it's relevant\n- **Error messages:** Users understand what failed and how to fix it\n- **Documentation sections:** Readers find what they need faster\n- **Code comments:** Maintainers understand the \"why\" before the \"how\"\n- **Commit messages:** Reviewers know the intent before reading code\n- **Proposals or plans:** Stakeholders see the recommendation before the analysis\n\nEvery document you write is a race against the reader's attention span. Front-load to win that race.\n","frontmatter":{"name":"front-loading","description":"Put the answer first, then context and details follow. Use when writing documentation, tutorials, error messages, or any communication where readers need to decide quickly if something is relevant to them."},"isInternal":false,"tokens":1683,"sizeBytes":7790},{"name":"SKILL.md","path":".claude/skills/optimize-benchmarks/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/optimize-benchmarks/SKILL.md","title":"optimize-benchmarks","category":"anthropic-skill","format":"markdown","content":"---\nname: optimize-benchmarks\ndescription: Iterative performance optimisation loop for syncpack-specifier. Runs benchmarks, identifies bottlenecks, applies optimisations, verifies tests pass and benchmarks improve, then repeats.\n---\n\n# Optimize Benchmarks\n\nIterative loop: benchmark, optimise, test, verify improvement, repeat.\n\n## Workflow\n\n### 1. Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --save-baseline before 2>&1 | tail -40\n```\n\nSave the output. Identify which variants are slowest.\n\n### 2. Identify Bottleneck\n\nFrom the full baseline, focus on the **slowest** benchmarks first.\n\nPriority order for specifier parsing:\n\n1. `Specifier::create` — the main parse function, called for every version string\n2. `parser::is_range` — checks 12 regexes sequentially\n3. `parser::is_exact` — checks 4 regexes sequentially\n4. `parser::is_complex_range` — splits, collects, iterates\n5. Individual regex matches in `regexes.rs`\n\n### 3. Apply ONE Optimisation\n\nMake a **single, focused change**. Do NOT bundle multiple optimisations — each must be independently measurable.\n\n### 4. Verify Tests Pass\n\n```bash\ncargo test -p syncpack-specifier 2>&1 | tail -5\n```\n\nIf tests fail, fix or revert. Never proceed with failing tests.\n\n### 5. Benchmark Against Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --baseline before 2>&1 | tail -40\n```\n\nLook for `[-XX.XXX% ...]` (improvement) or `[+XX.XXX% ...]` (regression).\n\n### 6. Evaluate\n\n- **Improved:** Report the gains. Update baseline: `cargo bench -p syncpack-specifier -- --save-baseline before`. Continue to step 2.\n- **No change:** Revert and try a different approach.\n- **Regressed:** Revert immediately.\n\n### 7. Repeat\n\nGo to step 2. Stop when:\n\n- User says stop\n- No bottlenecks remain\n- Gains are <1% across all benchmarks\n\n## Known Optimisation Opportunities\n\n### High Impact\n\n**Replace regex with char-based parsing in `parser.rs` / `regexes.rs`**\n\nMost regexes in `regexes.rs` match simple patterns like `^[0-9]+\\.[0-9]+\\.[0-9]+$` (exact semver). These can be replaced with byte/char iteration:\n\n```rust\n// Instead of regex EXACT: r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\"\nfn is_exact_version(s: &str) -> bool {\n  let mut dots = 0;\n  let bytes = s.as_bytes();\n  if bytes.is_empty() { return false; }\n  for &b in bytes {\n    match b {\n      b'0'..=b'9' => {},\n      b'.' => dots += 1,\n      _ => return false,\n    }\n  }\n  dots == 2\n}\n```\n\nRegex `is_match()` has overhead even for simple patterns: engine setup, capture group allocation. Char-based parsing for these patterns is 5-20x faster.\n\n**Reduce sequential regex attempts in `parser::is_range`**\n\n`is_range` tries 12 regexes. Instead, match on first char(s) to dispatch:\n\n```rust\nfn is_range(s: &str) -> bool {\n  match s.as_bytes().first() {\n    Some(b'^') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'~') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'>') => { /* check >= vs > then validate remainder */ },\n    Some(b'<') => { /* check <= vs < then validate remainder */ },\n    _ => false,\n  }\n}\n```\n\n**Consolidate related regex patterns**\n\nMany regexes are pairs: `EXACT` + `EXACT_TAG`, `CARET` + `CARET_TAG`, etc. Merge each pair into one function that handles both cases:\n\n```rust\nfn is_exact(s: &str) -> bool {\n  // Parse digits.digits.digits, then optionally -tag\n  let rest = parse_semver_triple(s)?;\n  rest.is_empty() || rest.starts_with('-')\n}\n```\n\n### Medium Impact\n\n**Replace `lazy_static` with `std::sync::OnceLock`**\n\n`lazy_static` uses an extra indirection layer. `OnceLock` (stable since Rust 1.80) is zero-cost after init:\n\n```rust\nuse std::sync::OnceLock;\n\nfn exact_regex() -> &'static Regex {\n  static RE: OnceLock<Regex> = OnceLock::new();\n  RE.get_or_init(|| Regex::new(r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\").unwrap())\n}\n```\n\nBut if regex is being replaced with char-based parsing, this becomes irrelevant.\n\n**Reorder checks in `Specifier::create` by frequency**\n\nIn a typical monorepo, most specifiers are `^x.y.z` (range) or `x.y.z` (exact). The current order already checks exact first, then range — good. But `is_exact` tries 4 regex patterns. A single fast char check can short-circuit:\n\n```rust\n// Fast path: first char is digit → likely exact or major or minor\n// Fast path: first char is ^ or ~ → likely range\n```\n\n**Avoid `String` allocation in `strip_semver_range`**\n\n`strip_semver_range` returns `&str` (already good), but callers like `Range::create` then `.to_string()` the result. Consider whether the allocation can be deferred.\n\n### Low Impact\n\n- Replace `HashMap` in caches with `FxHashMap` (faster hashing for short strings)\n- Use `SmallString` or stack-allocated strings for short specifiers\n- Pre-size cache `HashMap` with expected capacity\n\n## Architecture Notes\n\nKey files in `crates/syncpack-specifier/src/`:\n\n| File                         | Role                                                        |\n| ---------------------------- | ----------------------------------------------------------- |\n| `lib.rs`                     | `Specifier` enum, `create()` dispatch, caches               |\n| `parser.rs`                  | `is_exact()`, `is_range()`, etc. — classification functions |\n| `regexes.rs`                 | All `lazy_static` regex patterns                            |\n| `exact.rs`, `range.rs`, etc. | Variant constructors calling `node_semver`                  |\n| `semver_range.rs`            | `SemverRange` enum, `parse()`                               |\n\nThe hot path is: `Specifier::create()` → `parser::is_*()` → `regexes::*` → variant `::create()` → `node_semver` parsing.\n\nOptimising the `parser::is_*` layer gives the biggest wins because it runs for **every** specifier, and most of the time most checks return `false` (only one branch matches).\n\n## Rules\n\n- ONE change per iteration\n- Always verify tests pass before benchmarking\n- Always compare against baseline\n- Report numbers: `before → after (% change)`\n- Revert regressions immediately\n- Don't optimise what doesn't show up in benchmarks\n- Use **fast iteration** (`\"batch\"` filter) during the loop, **full suite** only at start and end\n","frontmatter":{"name":"optimize-benchmarks","description":"Iterative performance optimisation loop for syncpack-specifier. Runs benchmarks, identifies bottlenecks, applies optimisations, verifies tests pass and benchmarks improve, then repeats."},"isInternal":false,"tokens":1590,"sizeBytes":6131},{"name":"SKILL.md","path":".claude/skills/review-docs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/review-docs/SKILL.md","title":"review-docs","category":"anthropic-skill","format":"markdown","content":"---\nname: review-docs\ndescription: Review and improve Syncpack documentation for clarity, completeness, and consistency. Use when enhancing docs or validating before publication.\n---\n\n# Review Docs\n\nReview and improve Syncpack documentation against project standards.\n\n## When to Use\n\n- Validating docs before publication\n- Improving existing documentation\n- Ensuring consistency across pages\n- User reports documentation is unclear\n\n## Related Skills\n\nUse with:\n\n- `document-code` — Reference for structure and locations\n- `signal-over-noise` — Apply to remove filler and obvious explanations\n- `front-loading` — Ensure actionable information comes first\n\n## Your Task\n\nGiven a documentation path, review content against standards. Suggest concrete improvements.\n\n## Quality Checklist\n\n- [ ] **Clarity**: First line immediately shows what it covers\n- [ ] **Examples**: Practical, show real use cases, cover major patterns\n- [ ] **Completeness**: All relevant aspects documented\n- [ ] **Consistency**: Formatting matches other docs\n- [ ] **Conciseness**: No fluff; every sentence earns space\n- [ ] **Accessibility**: Users find what they need without deep reading\n- [ ] **Reuse**: Reference shared partials when available\n\n## Review Process\n\n1. Read the documentation file\n2. Cross-reference structure with similar docs\n3. Identify information gaps where users might get stuck\n4. Check for shared partials at `site/src/_partials/`\n5. Apply signal-over-noise: Can you cut 20% of words without losing meaning?\n\n## Review Output\n\nProvide structured feedback:\n\n**Current State**\n\n- Assessment of documentation quality and coverage\n\n**Issues Found**\n\n- Specific problems with locations (section names or line references)\n- Prioritize by impact: clarity > completeness > polish\n\n**Recommendations**\n\n- Concrete improvements with examples\n- Rewrite short sections if needed\n- Flag missing content or examples\n- Suggest shared partials to reuse\n\n**Priority Actions**\n\n- Top 2-3 changes to make immediately\n\n## Reference\n\n**Docs:** `site/src/content/docs/`\n**Shared partials:** `site/src/_partials/`\n**Source of truth:** `src/` (Rust implementation)\n**Local testing:** `cd site && pnpm run dev` then verify at `http://localhost:4321/syncpack`\n","frontmatter":{"name":"review-docs","description":"Review and improve Syncpack documentation for clarity, completeness, and consistency. Use when enhancing docs or validating before publication."},"isInternal":false,"tokens":479,"sizeBytes":2240},{"name":"SKILL.md","path":".claude/skills/search-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/search-code/SKILL.md","title":"search-code","category":"anthropic-skill","format":"markdown","content":"---\nname: search-code\ndescription: Search for code patterns in Syncpack. Use when finding symbols, implementations, or understanding how code is used. Covers ast-grep for Rust and grep/rg for other cases.\n---\n\n# Search Code\n\nGuide for searching the Syncpack codebase effectively.\n\n## Golden Rule\n\n**Use `ast-grep` for Rust files** — it filters out comments, strings, and docs.\n\n## Quick Reference\n\n| Looking for...                    | Use         |\n| --------------------------------- | ----------- |\n| Rust symbols, patterns, structs   | `ast-grep`  |\n| Multiple file types               | `grep`/`rg` |\n| Content in comments intentionally | `grep`/`rg` |\n| Non-Rust files (JSON, MD, TOML)   | `grep`/`rg` |\n\n## ast-grep Examples\n\n```bash\n# Find struct definitions\nast-grep -p 'pub struct $NAME' src/\n\n# Find function calls\nast-grep -p 'Specifier::new' src/\n\n# Find enum variants\nast-grep -p 'pub enum $NAME' src/\n\n# Find impl blocks\nast-grep -p 'impl $TYPE' src/\n\n# Find specific method definitions\nast-grep -p 'pub fn run($$$) -> i32' src/\n\n# Find match expressions\nast-grep -p 'match $EXPR { $$$ }' src/\n```\n\n## Pattern Syntax\n\n| Pattern | Matches                                      |\n| ------- | -------------------------------------------- |\n| `$NAME` | Single identifier                            |\n| `$$$`   | Zero or more items (arguments, fields, etc.) |\n| `$_`    | Any single expression                        |\n\n## When to Use grep/rg\n\n```bash\n# Search across file types\nrg \"TODO\" --type rust --type md\n\n# Search in comments (ast-grep ignores these)\nrg \"FIXME\" src/\n\n# Search non-Rust files\nrg \"version\" Cargo.toml\n\n# Case-insensitive search\nrg -i \"error\" src/\n```\n\n## Common Searches\n\n### Find where a type is used\n\n```bash\nast-grep -p 'Context' src/\n```\n\n### Find all InstanceState assignments\n\n```bash\nast-grep -p 'InstanceState::valid' src/\nast-grep -p 'InstanceState::fixable' src/\nast-grep -p 'InstanceState::unfixable' src/\n```\n\n### Find test files\n\n```bash\nfind src -name '*_test.rs'\n```\n\n### Find command implementations\n\n```bash\nast-grep -p 'pub fn run($$$) -> i32' src/commands/\n```\n\n## Tips\n\n- Start broad, then narrow with more specific patterns\n- Use `ast-grep` output to find the file, then `read_file` to understand context\n- Chain searches: find the type, then find where it's constructed\n","frontmatter":{"name":"search-code","description":"Search for code patterns in Syncpack. Use when finding symbols, implementations, or understanding how code is used. Covers ast-grep for Rust and grep/rg for other cases."},"isInternal":false,"tokens":604,"sizeBytes":2325},{"name":"SKILL.md","path":".claude/skills/signal-over-noise/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/signal-over-noise/SKILL.md","title":"signal-over-noise","category":"anthropic-skill","format":"markdown","content":"---\nname: signal-over-noise\ndescription: Maximize useful information per word by removing filler, obvious explanations, and hedging language. Use when writing documentation, error messages, code comments, or any communication where clarity and conciseness matter.\n---\n\n# Signal Over Noise\n\nMaximize useful information per word. Remove everything that doesn't earn its space.\n\n## What This Is\n\nSignal = information that moves the reader toward action or understanding.\nNoise = filler, explanations of obvious things, hedging language, unnecessary context.\n\nHigh signal-to-noise ratio means readers get what they need faster. Every word counts.\n\n## When to Use\n\n- Writing documentation, guides, skill descriptions\n- Creating error messages and feedback\n- Structuring arguments or explanations\n- Naming things (variables, functions, branches)\n- Any communication where clarity matters\n\n## How to Apply\n\n### 1. Remove Filler Words\n\n**High noise:**\n\n- \"basically\"\n- \"essentially\"\n- \"in order to\"\n- \"comprehensive\"\n- \"helpful\"\n- \"I think that\"\n- \"it could be argued that\"\n\n**Before:** \"Basically, you should essentially use this pattern in order to get comprehensive error handling.\"\n\n**After:** \"Use this pattern for error handling.\"\n\n### 2. Cut Explanations of Obvious Things\n\nDon't explain what readers already know. Assume competence.\n\n**Before:** \"Rust is a programming language. In Rust, you write code. This code runs on computers. When you compile Rust code, it produces a binary executable file.\"\n\n**After:** \"Compile Rust code to a binary with `cargo build --release`.\"\n\n### 3. Front-Load the Point\n\nPut the actionable information first. Context follows.\n\n**Before:** \"There are several reasons why you might want to consider testing your code. First, testing helps catch bugs. Second, it makes refactoring safer. Most importantly, tests serve as documentation. You should write tests.\"\n\n**After:** \"Write tests to catch bugs, enable safe refactoring, and document behavior.\"\n\n### 4. Use Active, Direct Language\n\n**High noise:** \"It is recommended that consideration be given to...\"\n\n**Signal:** \"Consider...\"\n\n**High noise:** \"The file was created by the user.\"\n\n**Signal:** \"Create the file.\"\n\n### 5. One Idea Per Sentence\n\nLong sentences hide the point. Break them.\n\n**Before:** \"When you're writing code that handles errors, which can happen in many places throughout your application, you should think about whether you want to handle them at the point where they occur or propagate them upward, and this decision affects how you structure your code.\"\n\n**After:** \"Handle errors where they occur or propagate them upward. This choice shapes your code structure. Decide early.\"\n\n### 6. Name Things Precisely\n\nNames are communication. Bad names add noise; good names clarify.\n\n**High noise:** `process_data()`, `handle_thing()`, `do_stuff()`\n\n**Signal:** `validate_user_input()`, `retry_failed_request()`, `parse_json_config()`\n\n### 7. Omit Context Readers Already Have\n\nDon't repeat what's obvious from surrounding text.\n\n**Before:** \"Here's an example of how to use the feature. In this example, we show you how to use it.\"\n\n**After:** \"Example:\"\n\n## Trade-offs\n\n### When NOT to apply aggressively\n\n- **Teaching beginners:** Some explanatory redundancy helps them build intuition\n- **Safety-critical systems:** Explicit redundancy catches misunderstanding\n- **Legal/compliance docs:** Specificity sometimes requires verbose phrasing\n- **User research notes:** Capture raw data, organize later\n\n### The balance\n\nSignal-over-noise isn't \"be terse.\" It's \"be useful.\" Sometimes that means:\n\n- Repeating a key phrase for emphasis\n- Providing context that prevents misreading\n- Showing your work so readers can verify reasoning\n- Acknowledging limitations explicitly\n\nRemove noise, keep signal. Don't remove useful context.\n\n## Examples\n\n### Code Documentation\n\n**Before:**\n\n```\nThis function is useful for validating email addresses.\nIt takes a string as input, which represents the email address\nthat you want to check. The function will return true if the\nemail is valid, or false if it is not valid. You might want to\nuse this function when you need to validate emails.\n```\n\n**After:**\n\n```\nValidate email addresses. Returns true if valid, false otherwise.\n```\n\n### Error Messages\n\n**Before:**\n\n```\nAn error has occurred in the system. The operation you attempted\ncould not be completed successfully. Please try again later or\ncontact support if the problem persists.\n```\n\n**After:**\n\n```\nFailed to fetch user data. Retry or contact support.\n```\n\n### Skill Description\n\n**Before:**\n\n```\nThis skill helps you think about how to communicate more effectively\nby using techniques that are designed to help you reduce unnecessary\ninformation and focus on the important parts of what you're trying\nto say so that your readers understand your message better and faster.\n```\n\n**After:**\n\n```\nMaximize useful information per word. Remove filler, obvious explanations, and hedging language. Every word should earn its space.\n```\n\n## Testing\n\nHow to know if you've applied this principle:\n\n1. **Read it aloud.** Does every sentence move you forward?\n2. **Ask \"why?\"** for each phrase. If the answer is \"readers already know this,\" cut it.\n3. **Count words.** Did you reduce the word count by 20–40%? Good sign.\n4. **Test comprehension.** Can a fresh reader understand it without re-reading?\n5. **Check names.** Are function/variable names predictable and specific?\n\n## Application in Syncpack\n\nThis principle strengthens all your other skills:\n\n- **write-code:** Function names, comments, commit messages all benefit\n- **document-code:** Docs communicate faster when noise is removed\n- **add-feature:** Feature descriptions and changelogs stay concise\n- **fix-bug:** Error messages become actionable, not explanatory\n- **search-code:** Clear naming makes code more discoverable\n\nApply signal-over-noise across the board. Your CLAUDE.md already mandates it—this skill formalizes how.\n","frontmatter":{"name":"signal-over-noise","description":"Maximize useful information per word by removing filler, obvious explanations, and hedging language. Use when writing documentation, error messages, code comments, or any communication where clarity and conciseness matter."},"isInternal":false,"tokens":1296,"sizeBytes":5984},{"name":"SKILL.md","path":".claude/skills/testing-gaps/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/testing-gaps/SKILL.md","title":"testing-gaps","category":"anthropic-skill","format":"markdown","content":"---\nname: testing-gaps\ndescription: Run coverage, inspect results, and identify missing test scenarios for a given source file. Use when analysing test coverage or finding untested branches.\n---\n\n# Testing Gaps\n\nFind untested branches and missing real-world test scenarios for a source file.\n\n## Workflow\n\n1. **Run text coverage** for the target file\n2. **Identify uncovered branches** from the output\n3. **Cross-reference** with test file to understand what's tested\n4. **Report** uncovered branches + missing real-world scenarios\n\n## Step 1: Run Coverage\n\nUse `cargo llvm-cov` with `--text` output (not `--html`) for parseable results.\n\n```bash\n# Full coverage, filtered to target file\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/TARGET_FILE\\.rs:/,/^$/p'\n```\n\nExample for `preferred_semver.rs`:\n\n```bash\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/preferred_semver\\.rs:/,/^$/p'\n```\n\n## Step 2: Read Coverage Output\n\nThe text format shows per-line execution counts:\n\n```\n  42|     80|        .and_then(|range| ...)     # Hit 80 times\n  74|      0|        Some(preferred)            # Never hit\n 321|      0|        instance.mark_conflict(...) # Never hit\n```\n\n- `count > 0` = covered\n- `count = 0` = uncovered branch\n- `^N` annotations on sub-expressions show partial coverage within a line\n\n### Extract only uncovered lines\n\n```bash\n... | grep -E '^\\s+\\d+\\|\\s+0\\|'\n```\n\n## Step 3: Classify Gaps\n\nFor each uncovered branch, determine:\n\n1. **What code path leads here?** — Trace the `if/else` chain backward\n2. **What input would trigger it?** — What package.json + config combination\n3. **Is it reachable?** — Some branches may be defensive/unreachable\n4. **Is it worth testing?** — Real-world scenario vs theoretical edge case\n\n### Categories\n\n| Category                         | Action            |\n| -------------------------------- | ----------------- |\n| Real-world scenario never tested | Write a test      |\n| Edge case in existing logic      | Write a test      |\n| Defensive branch (unreachable)   | Note but skip     |\n| Dead code                        | Consider removing |\n\n## Step 4: Identify Missing Scenarios\n\nBeyond line coverage, look for missing _combinations_:\n\n- Feature A tested, Feature B tested, A+B never tested together\n- Only tested with one package, never with multiple\n- Only tested with `highestSemver`, never `lowestSemver`\n- Only tested with one range type (`^`), never others (`>=`, `<=`)\n- Only tested without semver groups, never with\n\n## Step 5: Report\n\nStructure findings as:\n\n1. **Uncovered branches** — Specific lines, what triggers them, why untested\n2. **Missing real-world scenarios** — Combinations and interactions not covered\n\n## Running Tests After Adding Coverage\n\n```bash\njust test                              # All tests\ncargo test test_name -- --nocapture   # Specific test\njust coverage                         # Regenerate coverage report\n```\n","frontmatter":{"name":"testing-gaps","description":"Run coverage, inspect results, and identify missing test scenarios for a given source file. Use when analysing test coverage or finding untested branches."},"isInternal":false,"tokens":762,"sizeBytes":3074},{"name":"SKILL.md","path":".claude/skills/write-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/write-code/SKILL.md","title":"write-code","category":"anthropic-skill","format":"markdown","content":"---\nname: write-code\ndescription: Rust code style and conventions for Syncpack. Use when writing or modifying Rust code. Covers functional patterns, imports, naming, and quality standards.\n---\n\n# Write Code\n\nRust code conventions for Syncpack.\n\n## Style\n\n- **Functional style:** pipelines over loops\n- **Avoid `?` chains:** use `.and_then()`, `.map()`, `.or_else()`\n- **Descriptive names:** clarity over brevity\n- **Named placeholders:** `println!(\"{var}\")` not `println!(\"{}\", var)`\n- **British English:** \"behaviour\" not \"behavior\", \"organised\" not \"organized\"\n\n## Imports\n\nSingle `use` statement with grouped braces:\n\n```rust\nuse {\n  crate::{cli::Cli, config::Config},\n  log::{debug, error},\n  std::{process::exit, sync::Arc},\n};\n```\n\nRules:\n\n- **Never use `super::`** — always `crate::` for internal imports\n- Group: `crate::`, external crates, `std::`\n- Alphabetise within groups\n\n## File Organisation\n\n| Adding...   | Location                                                         |\n| ----------- | ---------------------------------------------------------------- |\n| New command | `src/commands/{name}.rs`                                         |\n| New test    | Sibling `_test.rs` file (e.g., `src/foo.rs` → `src/foo_test.rs`) |\n\n**NEVER** use `#[cfg(test)]` modules inside implementation files.\n\n## Quality\n\n- Functions <50 lines, commands 100-300 lines\n- Zero warnings (except during TDD red phase)\n- Run `just format` before committing\n\n## Comments\n\n### Forbidden\n\n- **`//!` module docs** — collect implementation history and rot\n- **Phase/F-N labels** — `// Phase v4-2`, `// F16:`, banner headers like `// ---------- Phase v4-3 RED ----------`\n- **History refs** — `Migrated from`, `carry-over`, `Mirrors v3`, `replaces the v3`, `previously inside X`, `reserved for future`\n- **Issue/PR refs** — `Reproduces issue #239`, `GitHub issue #206`. Plans rot; the code is the truth\n- **Banner separators** — `// ---------- Free functions: yaml ops ----------`. Use module structure or let the file speak for itself\n- **Test scenario labels** — don't write `// Windows-style backslashes` above a test input. Use a descriptive variable binding (`let windows_backslashes = [...]`) instead\n- **Name-restating field docs** — `/// A unique identifier for this instance`, `/// The dependency name`, `/// The instance id`. The field name already says it\n- **Step numbering inside functions** — `// 1. ...`, `// 2. ...`. Drop the numbers; keep the WHY if it's non-obvious\n\n### Allowed\n\n- **`@TODO` markers** — future plans worth tracking inline\n- **`///` doc comments** for non-obvious detail: `None`/`Err` semantics, side effects, invariants, distinctions between similar fields (e.g. `is_local_dependency` vs `is_local_instance`)\n- **`//` inline comments** when WHY is non-obvious: hidden constraints, ordering invariants (\"SnappedTo groups must be visited last\"), workarounds for surprising library behaviour, \"must not poison X\" cautions\n\n### Default\n\nNo comments. Add one only when the WHY is non-obvious to a fresh reader.\n\n## Patterns\n\n### Iterating Instances\n\n```rust\nctx.version_groups.iter().for_each(|group| {\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n        dependency.get_sorted_instances()\n            .filter(|instance| instance.is_invalid())\n            .for_each(|instance| { /* process */ });\n    });\n});\n```\n\n### Error Handling\n\nPrefer combinators over `?`:\n\n```rust\n// Good\npath.parent()\n    .and_then(|p| p.to_str())\n    .map(|s| s.to_string())\n    .unwrap_or_default()\n\n// Avoid\nlet parent = path.parent()?;\nlet str = parent.to_str()?;\nOk(str.to_string())\n```\n\n### State Mutation\n\n```rust\nlet mut state = instance.state.borrow_mut();\nif !state.is_invalid() {\n    *state = InstanceState::fixable(SomeVariant);\n}\n```\n","frontmatter":{"name":"write-code","description":"Rust code style and conventions for Syncpack. Use when writing or modifying Rust code. Covers functional patterns, imports, naming, and quality standards."},"isInternal":false,"tokens":919,"sizeBytes":3797},{"name":"patterns.md","path":".claude/skills/write-tests/patterns.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/write-tests/patterns.md","title":"Write-tests Skill","category":"anthropic-skill","format":"markdown","content":"# Test patterns\n\nReal, trimmed examples for the common scenarios. Imports and `mod` wrapping omitted for brevity — see [SKILL.md](SKILL.md) for the full preamble.\n\n## Banned\n\n`src/version_group/banned_test.rs`. Removes a dep; refuses to ban a local-package version.\n\n```rust\n#[tokio::test]\nasync fn refuses_to_ban_local_version() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"version\": \"1.0.0\"}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"package-a\": \"1.1.0\"}}),\n    ])\n    .with_version_group(json!({\n      \"dependencies\": [\"package-a\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::suspect(InvalidLocalVersion),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::suspect(RefuseToBanLocal),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"package-a\",\n      id: \"package-a in /dependencies of package-b\",\n      actual: \"1.1.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Banned with `customTypes`\n\n```rust\n#[tokio::test]\nasync fn removes_instance_with_named_version_string_strategy() {\n  let ctx = TestBuilder::new()\n    .with_config(json!({\n      \"customTypes\": {\n        \"packageManager\": {\n          \"strategy\": \"name@version\",\n          \"path\": \"packageManager\"\n        }\n      }\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"packageManager\": \"pnpm@7.27.0\"\n    })])\n    .with_version_group(json!({\n      \"dependencies\": [\"pnpm\"],\n      \"dependencyTypes\": [\"packageManager\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"pnpm\",\n      id: \"pnpm in /packageManager of package-a\",\n      actual: \"7.27.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Pinned\n\n`src/version_group/pinned_test.rs`.\n\n```rust\n#[tokio::test]\nasync fn an_already_pinned_version_is_valid() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"1.2.0\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIdenticalToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"1.2.0\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Pin overrides a semver-group range\n\n```rust\n#[tokio::test]\nasync fn pin_overrides_semver_group_match() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"^1.0.0\"}\n    }))\n    .with_semver_group(json!({\"range\": \"^\", \"dependencies\": [\"foo\"]}))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.0.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::fixable(PinOverridesSemverRange),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"^1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: Some(\"^1.0.0\"),\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Same range\n\n`src/version_group/same_range_test.rs`. Use a `local`-ignored group as guard so the local instances don't pollute the assertion.\n\n```rust\n#[tokio::test]\nasync fn ranges_satisfy_each_other() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"<=2.0.0\"}}),\n    ])\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-a\",\n      actual: \">=1.0.0\",\n      expected: Some(\">=1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-b\",\n      actual: \"<=2.0.0\",\n      expected: Some(\"<=2.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nFor mismatching ranges the consumer instances become `InstanceState::unfixable(SameRangeMismatch)`.\n\n## Pnpm catalogs\n\n`src/version_group/catalog_test.rs`. `with_pnpm_catalogs` injects `pnpm-workspace.yaml`. Catalog definitions are themselves instances under `/catalog of pnpm-workspace.yaml`.\n\n```rust\n#[tokio::test]\nasync fn catalog_group_definition_valid() {\n  let yaml = \"catalog:\\n  react: ^18.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_version_group(json!({\n      \"label\": \"enforce catalog\",\n      \"dependencies\": [\"react\"],\n      \"policy\": \"catalog\",\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalogDefinition),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^18.0.0\",\n      expected: Some(\"^18.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nNamed catalog: yaml `catalogs:\\n  react18:\\n    react: ^18.0.0\\n`, consumer `\"react\": \"catalog:react18\"`, def id `react in /catalogs/react18 of pnpm-workspace.yaml`.\n\n## Bun catalogs\n\n`src/version_group/bun_catalog_test.rs`. Two shapes:\n\n- `with_bun_catalogs(json!({...}))` puts `/catalog`, `/catalogs/{n}` at the synthetic Bun root.\n- `with_bun_workspaces_catalogs(json!({...}))` nests them under `/workspaces/`.\n\nThese tests often use sync `.build()` — they're checking discovery wiring, not the visit pipeline:\n\n```rust\n#[test]\nfn bun_top_level_catalog_definition_emits_instance() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\"name\": \"pkg-a\", \"version\": \"0.0.0\"})])\n    .build();\n  let react = ctx\n    .instances\n    .iter()\n    .find(|i| i.descriptor.name == \"react\")\n    .expect(\"expected a react bun catalog instance\");\n  assert!(react.is_catalog_instance());\n  assert_eq!(react.descriptor.specifier.get_raw(), \"^18.0.0\");\n}\n```\n\nFull validation pass (consumer + def states):\n\n```rust\n#[test]\nfn bun_catalog_definition_with_consumer_valid() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"}\n    })])\n    .build_and_visit_packages();\n  // assert states via expect(&ctx).to_have_instances(...) as elsewhere\n}\n```\n\n## Semver groups\n\nA semver group rewrites the *expected* range; mismatches are `SemverRangeMismatch` (fixable). Combine with version groups to test interaction:\n\n```rust\n#[tokio::test]\nasync fn semver_group_targets_one_package_only() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"^1.2.3\"}}),\n    ])\n    .with_semver_group(json!({\"packages\": [\"package-b\"], \"range\": \"^\"}))\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  // package-b's `^1.2.3` matches the `^` semver group → no mismatch\n  // assert SatisfiesSameRangeGroup on both consumer instances\n}\n```\n\n## Registry updates\n\n`with_registry_updates` mocks the npm registry and implies subcommand `update`. Optional `with_update_target` bounds it.\n\n```rust\n#[tokio::test]\nasync fn pnpm_catalog_def_marked_outdated_when_registry_has_newer_version() {\n  let yaml = \"catalog:\\n  react: ^17.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_registry_updates(json!({\"react\": [\"17.0.0\", \"18.5.0\"]}))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToNpmRegistry),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^17.0.0\",\n      expected: Some(\"^18.5.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nBound the update target:\n\n```rust\nlet ctx = TestBuilder::new()\n  .with_package(json!({\"name\": \"package-a\", \"dependencies\": {\"wat\": \"1.2.3\"}}))\n  .with_update_target(UpdateTarget::Minor)\n  .with_registry_updates(json!({\"wat\": [\"1.2.3\", \"1.3.4\", \"2.0.0\"]}))\n  .run()\n  .await;\n// wat → 1.3.4 (minor cap), not 2.0.0\n```\n\n`UpdateTarget` import: `use crate::cli::UpdateTarget;`.\n","isInternal":false,"tokens":3249,"sizeBytes":11805},{"name":"SKILL.md","path":".claude/skills/write-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JamieMason/syncpack/HEAD/.claude/skills/write-tests/SKILL.md","title":"write-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: write-tests\ndescription: Write tests for Syncpack using the TestBuilder pattern. Use when adding tests for commands, validation logic, or any new functionality. Covers TestBuilder API, assertion patterns, and common test scenarios.\n---\n\n# Write Tests\n\n## Golden rules\n\n- Use `TestBuilder` — never construct `Context` manually.\n- Assert with `expect(&ctx).to_have_instances(vec![...])` — never index `ctx.instances`.\n- Async-first: `#[tokio::test]` + `.run().await` is the canonical entry.\n\n## TDD\n\n1. Read 2-3 tests in the same `*_test.rs` and copy the pattern\n2. Write failing test → `just test` → confirm RED\n3. Ask before implementing\n4. Implement minimal code → GREEN → `just format`\n\n## Quick start\n\n```rust\nuse {\n  crate::{\n    instance::{FixableInstance::*, InstanceState, SuspectInstance::*, UnfixableInstance::*, ValidInstance::*},\n    test::{\n      builder::TestBuilder,\n      expect::{expect, ExpectedInstance},\n    },\n  },\n  serde_json::json,\n};\n\n#[tokio::test]\nasync fn pinned_version_replaces_anything_different() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"workspace:*\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"workspace:*\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Sub-module organisation\n\nGroup related scenarios under nested modules — see `pinned_test.rs`, `catalog_defs_test.rs`:\n\n```rust\nmod local {\n  use super::*;\n  #[tokio::test] async fn refuses_to_pin_local_version() { ... }\n}\n\nmod normal {\n  use super::*;\n  #[tokio::test] async fn an_already_pinned_version_is_valid() { ... }\n}\n\nmod registry_updates {\n  use super::*;\n  #[tokio::test] async fn def_marked_outdated_when_registry_has_newer_version() { ... }\n}\n```\n\n## File location\n\n| Test type                                    | Location                                    |\n| -------------------------------------------- | ------------------------------------------- |\n| Version-group behaviour (pin, ban, ranges …) | `src/version_group/<group>_test.rs`         |\n| Catalog discovery wiring                     | `src/version_group/{catalog,bun_catalog}_test.rs` |\n| Fix mutations                                | `src/commands/fix_test.rs`                  |\n| Format pass                                  | `src/visit_formatting/format_test.rs`       |\n| Other unit tests                             | Co-located: `src/foo.rs` ↔ `src/foo_test.rs` |\n\n## Builder methods\n\nSource of truth: `src/test/builder.rs`.\n\n| Method                                                  | Purpose                                                              |\n| ------------------------------------------------------- | -------------------------------------------------------------------- |\n| `.with_package(json!({...}))`                           | Add one package.json                                                 |\n| `.with_packages(vec![...])`                             | Add many                                                             |\n| `.with_version_group(json!({...}))`                     | Add one version group                                                |\n| `.with_version_groups(vec![...])`                       | Add many                                                             |\n| `.with_semver_group(json!({...}))`                      | Add a semver group                                                   |\n| `.with_config(json!({...}))`                            | Base config (e.g. `customTypes`, `dependencyGroups`)                 |\n| `.with_strict(bool)`                                    | Strict mode (Suspect → error)                                        |\n| `.with_subcommand(\"update\")`                            | Override subcommand (default: `lint`, or `update` if registry set)   |\n| `.with_pnpm_catalogs(yaml)`                             | Inject `pnpm-workspace.yaml`; implies pnpm PM                        |\n| `.with_bun_catalogs(json!({...}))`                      | Synthetic Bun root with `/catalog`, `/catalogs/{n}`; implies Bun PM  |\n| `.with_bun_workspaces_catalogs(json!({...}))`           | Same, nested under `/workspaces/`                                    |\n| `.with_{pnpm,bun,npm,yarn,unknown}_package_manager()`   | Force PM detection                                                   |\n| `.with_registry_updates(json!({\"react\":[...]}))`        | Mock npm registry; implies subcommand=`update`                       |\n| `.with_update_target(UpdateTarget::Minor)`              | Bound update target                                                  |\n| `.run().await` → `Context`                              | **Primary** end-to-end (full pipeline through disk + discovery)      |\n| `.build()`                                              | Sync, no visit — context-wiring tests                                |\n| `.build_and_visit_packages()`                           | Sync + visit_packages — fix tests, older suites                      |\n| `.build_and_visit_formatting()`                         | Sync + visit_formatting                                              |\n| `.build_with_registry_and_visit().await`                | Sync wiring + async registry mock + visit                            |\n\n## ExpectedInstance fields\n\n```rust\nExpectedInstance {\n  state: InstanceState::fixable(DiffersToPin),  // valid / fixable / unfixable / suspect\n  dependency_name: \"react\",                     // = `internal_name` (alias-aware)\n  id: \"react in /dependencies of package-a\",    // {dep} in {/path} of {package_or_yaml}\n  actual: \"17.0.0\",                             // raw specifier on disk\n  expected: Some(\"18.0.0\"),                     // None = ignore; Some(\"\") = remove\n  overridden: None,                             // semver-group override target, if any\n}\n```\n\n`id` location examples:\n\n- `/dependencies`, `/devDependencies`, `/peerDependencies`\n- `/version of package-a` (local version)\n- `/packageManager`, `/engines/node`\n- `/catalog of pnpm-workspace.yaml`, `/catalogs/<name> of pnpm-workspace.yaml`\n- `/customVersion`, `/custom/config/version` (via `customTypes`)\n\n## Patterns\n\n→ [patterns.md](patterns.md): banned, pinned, sameRange, pnpm catalogs, bun catalogs, semver ranges, registry updates.\n\n## Fix tests\n\n`src/commands/fix_test.rs` builds with `.build_and_visit_packages()` then runs `fix::run(ctx, &SilentReporter, &disk)`. `dry_run = true` is the default (set by `mock::config_from_mock`), so `is_dirty()` and post-fix contents stay observable. Set `ctx.config.cli.dry_run = false` only when asserting writes through a recording `MockDiskIo` (see `pnpm_fix_writes_yaml_to_disk`).\n\n## Common mistakes\n\n| Wrong                                            | Right                                                                                  |\n| ------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| `#[test] fn foo()` + `.run().await`              | `#[tokio::test] async fn foo()`                                                        |\n| `use crate::instance_state::*`                   | `use crate::instance::*`                                                               |\n| `\"pinned\": \"1.0.0\"`                              | `\"pinVersion\": \"1.0.0\"`                                                                |\n| `Context { ... }`                                | `TestBuilder::new()...`                                                                |\n| `ctx.instances[0]`                               | `expect(&ctx).to_have_instances(vec![...])`                                            |\n| `.build()` then check states                     | `.run().await` (or `.build_and_visit_packages()` for sync)                             |\n| Missing `SuspectInstance::*` import              | Import all 4: `FixableInstance::*, ValidInstance::*, SuspectInstance::*, UnfixableInstance::*` |\n\n## Running\n\n```bash\njust test                            # all\ncargo test pinned_test               # pattern match\ncargo test test_name -- --nocapture  # with stdout\n```\n\n## Reference tests\n\n- `src/version_group/banned_test.rs` — banned + custom types\n- `src/version_group/pinned_test.rs` — sub-modules, semver-group interaction\n- `src/version_group/same_range_test.rs` — range satisfaction\n- `src/version_group/catalog_test.rs` — pnpm catalogs\n- `src/version_group/bun_catalog_test.rs` — bun catalogs (sync `.build()`)\n- `src/version_group/preferred_semver_test.rs` — registry updates, update targets\n","frontmatter":{"name":"write-tests","description":"Write tests for Syncpack using the TestBuilder pattern. Use when adding tests for commands, validation logic, or any new functionality. Covers TestBuilder API, assertion patterns, and common test scenarios."},"isInternal":false,"tokens":1880,"sizeBytes":9187}],"systemPromptSnippet":"<yakaai_skills repo=\"JamieMason/syncpack\">\n<!-- File: .claude/rules/syncpack.md (Tokens: ~447 | Category: anthropic-skill) -->\n# Syncpack\n\nRust CLI tool for synchronising npm dependency versions across JavaScript monorepos.\n\n## Getting Started\n\n1. Run `just` to see available commands\n2. Read `.notes/` for architecture and mental model\n3. Check `skills/` for task-specific guides:\n\n## Communication Style\n\n- Signal over noise\n- Extremely concise. Sacrifice grammar for brevity\n- Action-oriented: what to DO\n- Grounded in facts: cite code/docs\n- BANNED WORDS: basically, essentially, in order to, comprehensive\n- Always use AskUserQuestion when asking questions\n\n## Pre-Implementation Checklist\n\nBefore large changes, answer YES to all:\n\n- Have I identified ALL decision points?\n- Have I listed trade-offs for each approach?\n- Have I asked user which strategy to use?\n- Am I NOT making architectural assumptions?\n- Am I NOT creating files user didn't request?\n\n## Hard Rules: Ask First\n\nMUST ask when:\n\n- User intent unclear or multiple valid approaches exist\n- Breaking changes or core architecture modifications\n- Creating ANY new files/modules not explicitly requested\n- Large refactors (identify ALL decision points first)\n\nMUST NOT:\n\n- Use banned words\n- Assume user wants plan or summary documentation\n- Refactor without asking\n- Make architectural decisions unilaterally\n\n## Proceed Without Asking\n\n- Pattern clearly exists in codebase\n- Following established convention\n- Non-breaking changes\n- Adding tests, fixing obvious bugs\n\n## Documentation Rules: MANDATORY\n\n**NEVER create new documentation files unless explicitly requested.**\n\nExamples of BANNED actions:\n\n- Creating \"status\" docs, \"validation\" docs, \"current-status\" files\n- Creating \"summary\" docs, \"guide\" docs, \"checklist\" files\n- Splitting existing docs into multiple files\n- Creating \"handoff\" docs, \"next steps\" docs\n\n**ONLY update existing docs:**\n\n- Update the original document in place\n- Add sections/checkboxes to existing files\n- Mark tasks complete where they already exist\n\n**If user has ONE plan file, keep ONE plan file.**\n\nDo NOT proliferate documentation. Update existing files only.\n\n<!-- File: .claude/skills/add-feature/adding-command.md (Tokens: ~2113 | Category: anthropic-skill) -->\n# Example: Adding a New Command\n\n<scenario>\nThis guide walks through adding a new `syncpack check` command that lists outdated dependencies without modifying files.\n</scenario>\n\n<step number=\"1\">\n\n## Step 1: Decide on the Visitor\n\n**Question:** Does this command deal with dependency versions or package.json formatting?\n\n**Answer:** Dependency versions → Use `visit_packages`\n\n</step>\n\n<step number=\"2\">\n\n## Step 2: Add Command Enum Variant\n\n**File:** `src/cli.rs`\n\n```rust\npub enum Subcommand {\n  Lint,\n  Fix,\n  Format,\n  Update,\n  List,\n  Json,\n  Check,  // ← Add this\n}\n```\n\n</step>\n\n<step number=\"3\">\n\n## Step 3: Create Command Implementation\n\n**File:** `src/commands/check.rs`\n\n<implementation>\n\n```rust\nuse crate::{commands::ui, context::Context};\n\n/// List outdated dependencies without modifying files\npub fn run(ctx: Context) -> i32 {\n  let mut has_outdated = false;\n\n  ctx.version_groups.iter().for_each(|group| {\n    let mut has_printed_group = false;\n\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n      let mut has_printed_dependency = false;\n\n      dependency\n        .get_sorted_instances()\n        .filter(|instance| instance.is_outdated())\n        .for_each(|instance| {\n          // Lazy print headers only when we find outdated instances\n          if !has_printed_group {\n            ui::group::print_header(&ctx, group);\n            has_printed_group = true;\n          }\n          if !has_printed_dependency {\n            ui::dependency::print(&ctx, dependency, &group.variant);\n            has_printed_dependency = true;\n          }\n\n          // Print the outdated instance\n          if ctx.config.cli.show_instances {\n            ui::instance::print(&ctx, instance, &group.variant);\n          }\n\n          has_outdated = true;\n        });\n    });\n  });\n\n  if has_outdated {\n    1  // Exit with error code\n  } else {\n    ui::util::print_no_issues_found();\n    0  // Exit successfully\n  }\n}\n```\n\n</implementation>\n\n<pattern_notes>\n\n**Pattern notes:**\n\n- Take ownership of `Context`\n- Return `i32` exit code (0 = success, 1 = failure)\n- Use lazy printing (only print headers when needed)\n- Iterate: version groups → dependencies → instances\n- Filter instances by state (`.filter(|instance| instance.is_outdated())`)\n- Use existing UI utilities for consistent output\n\n</pattern_notes>\n\n</step>\n\n<step number=\"4\">\n\n## Step 4: Register Command in Module\n\n**File:** `src/commands.rs`\n\n```rust\npub mod fix;\npub mod format;\npub mod json;\npub mod lint;\npub mod list;\npub mod update;\npub mod check;  // ← Add this\n```\n\n</step>\n\n<step number=\"5\">\n\n## Step 5: Add to Main Dispatch\n\n**File:** `src/main.rs`\n\n```rust\nuse {\n  crate::{\n    commands::{check, fix, format, json, lint, list, update},  // ← Add check\n    // ... other imports\n  },\n};\n\n// ... in main() function:\n\nlet _exit_code = match ctx.config.cli.subcommand {\n  Subcommand::Fix => {\n    let ctx = visit_packages(ctx);\n    fix::run(ctx)\n  }\n  Subcommand::Format => {\n    let ctx = visit_formatting(ctx);\n    format::run(ctx)\n  }\n  Subcommand::Lint => {\n    let ctx = visit_packages(ctx);\n    lint::run(ctx)\n  }\n  Subcommand::Update => {\n    let mut ctx = ctx;\n    ctx.fetch_all_updates().await;\n    let ctx = visit_packages(ctx);\n    update::run(ctx)\n  }\n  Subcommand::List => {\n    let ctx = visit_packages(ctx);\n    list::run(ctx)\n  }\n  Subcommand::Json => {\n    let ctx = visit_packages(ctx);\n    json::run(ctx)\n  }\n  Subcommand::Check => {  // ← Add this\n    let ctx = visit_packages(ctx);\n    check::run(ctx)\n  }\n};\n```\n\n<key_points>\n\n**Key points:**\n\n- Must call `visit_packages(ctx)` before command (to assign InstanceStates)\n- Pass the returned Context to your command's run function\n- The command consumes Context and returns exit code\n\n</key_points>\n\n</step>\n\n<step number=\"6\">\n\n## Step 6: Add CLI Help Text (Optional)\n\n**File:** `src/cli.rs`\n\n```rust\n#[derive(Debug, clap::Parser)]\n#[command(name = \"syncpack\")]\n#[command(about = \"Manage multiple package.json files\")]\npub struct Cli {\n  #[command(subcommand)]\n  pub subcommand: Subcommand,\n  // ... other fields\n}\n\n#[derive(Debug, clap::Subcommand)]\npub enum Subcommand {\n  /// Find and report outdated dependencies\n  #[command(name = \"check\")]\n  Check,\n  // ... other variants\n}\n```\n\n</step>\n\n<step number=\"7\">\n\n## Step 7: Write Tests\n\n**File:** `src/commands/check_test.rs` (or add to existing test file)\n\n<test_examples>\n\n```rust\nuse {\n  crate::{\n    commands::check,\n    instance_state::{FixableInstance::*, InstanceState, ValidInstance::*},\n    test::{builder::TestBuilder, expect::ExpectedInstance},\n  },\n  serde_json::json,\n};\n\n#[test]\nfn reports_outdated_dependencies() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"17.0.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"17.0.0\", \"17.0.1\", \"18.0.0\", \"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  // Verify the instance is marked as outdated\n  let react_instance = ctx.instances.iter()\n    .find(|i| i.dependency.name == \"react\")\n    .expect(\"Should find react instance\");\n\n  assert!(react_instance.is_outdated());\n\n  // Run the command (in real test, you'd capture stdout)\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 1, \"Should exit with error when outdated found\");\n}\n\n#[test]\nfn exits_successfully_when_up_to_date() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\n        \"name\": \"package-a\",\n        \"dependencies\": {\n          \"react\": \"18.2.0\"\n        }\n      }),\n    ])\n    .with_registry_updates(json!({\n      \"react\": [\"18.2.0\"]\n    }))\n    .build_and_visit_packages();\n\n  let exit_code = check::run(ctx);\n  assert_eq!(exit_code, 0, \"Should exit successfully when up to date\");\n}\n```\n\n</test_examples>\n\n</step>\n\n<step number=\"8\">\n\n## Step 8: Test Locally\n\n```bash\n# Build and run\ncargo run -- check --help\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- check\n\n# Run tests\ncargo test check\njust test\n```\n\n</step>\n\n<step number=\"9\">\n\n## Step 9: Verify All Places Updated\n\n<checklist>\n\n- [x] Added to `Subcommand` enum in `src/cli.rs`\n- [x] Created `src/commands/check.rs` with `pub fn run(ctx: Context) -> i32`\n- [x] Registered in `src/commands.rs` module\n- [x] Added match arm in `src/main.rs`\n- [x] Called appropriate visitor (`visit_packages`)\n- [x] Written tests\n- [x] Tested locally\n\n</checklist>\n\n</step>\n\n<variations>\n\n## Common Variations\n\n<variation type=\"file_modification\">\n\n### Command that modifies files\n\n```rust\npub fn run(mut ctx: Context) -> i32 {\n  // ... iterate and modify instances\n\n  // Write changes to disk\n  ctx.packages.write_all();\n\n  0\n}\n```\n\n</variation>\n\n<variation type=\"registry\">\n\n### Command that needs registry data\n\nIn `main.rs`:\n\n```rust\nSubcommand::MyCommand => {\n  let mut ctx = ctx;\n  ctx.fetch_all_updates().await;  // Fetch from npm\n  let ctx = visit_packages(ctx);\n  my_command::run(ctx)\n}\n```\n\n</variation>\n\n<variation type=\"cli_options\">\n\n### Command with specific CLI options\n\n```rust\npub enum Subcommand {\n  #[command(name = \"check\")]\n  Check {\n    /// Only check production dependencies\n    #[arg(long)]\n    prod_only: bool,\n  },\n}\n```\n\nAccess in command:\n\n```rust\nif let Subcommand::Check { prod_only } = &ctx.config.cli.subcommand {\n  if *prod_only {\n    // Filter logic\n  }\n}\n```\n\n</variation>\n\n</variations>\n\n<troubleshooting>\n\n## Troubleshooting\n\n<problem>\n**Problem:** Command not found\n→ Check you added it to all three places: enum, main.rs match, commands module\n</problem>\n\n<problem>\n**Problem:** Instances have `Unknown` state\n→ You forgot to call `visit_packages(ctx)` before your command\n</problem>\n\n<problem>\n**Problem:** Compilation error about moved value\n→ Make sure your command signature is `pub fn run(ctx: Context) -> i32`\n</problem>\n\n<problem>\n**Problem:** Changes not persisting\n→ You need to call `ctx.packages.write_all()` in commands that modify files\n</problem>\n\n</troubleshooting>\n\n<related_examples>\n\n## Related Examples\n\n- See `src/commands/lint.rs` for read-only command\n- See `src/commands/fix.rs` for file-modifying command\n- See `src/commands/update.rs` for command using registry client\n\n</related_examples>\n\n<!-- File: .claude/skills/add-feature/adding-instance-state.md (Tokens: ~2551 | Category: anthropic-skill) -->\n# Adding a New InstanceState Variant\n\n<scenario>\nThis guide walks through adding new `InstanceState` variants. The example used\nthroughout is `SameMinorHasMajorMismatch`, a real variant added during the\nsameMinor improvement work. It is an `Unfixable` state assigned when instances\nin a `sameMinor` version group have different MAJOR versions.\n</scenario>\n\n---\n\n## Step 1: Choose the right category\n\n```\nInstanceState\n├── Unknown              — never assign this; it is the initial pre-inspection state\n├── Valid(ValidInstance) — instance follows all rules correctly\n├── Invalid(InvalidInstance)\n│   ├── Fixable(FixableInstance)                  — syncpack knows the correct value and can auto-fix\n│   ├── Unfixable(UnfixableInstance)              — ambiguous; needs human decision\n│   └── Conflict(SemverGroupAndVersionConflict)   — two active rules are simultaneously unsatisfiable\n└── Suspect(SuspectInstance)                      — user misconfiguration\n```\n\nAsk:\n\n- Do we know the correct value? → `Fixable`\n- Is the situation ambiguous, multiple valid options? → `Unfixable`\n- Are two rules active and irreconcilable? → `Conflict`\n- Has the user misconfigured something? → `Suspect`\n- Is everything fine? → `Valid`\n\nFor `SameMinorHasMajorMismatch`: crossing a major version boundary is unsafe\nand we cannot pick a winner without asking the user → `Unfixable`.\n\n---\n\n## Step 2: Add the variant to `src/instance_state.rs`\n\nFind the appropriate enum and add the variant with a multi-line doc comment.\nEvery condition relevant to the state must be listed. Use these symbols:\n\n- `✓` — condition is true / requirement is met\n- `✘` — condition is false / requirement is violated\n- `?` — ambiguous / unknown / why we cannot auto-fix\n- `!` — notable side effect or policy decision\n\n```rust\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum UnfixableInstance {\n  // ... existing variants ...\n\n  /// - ✘ Instance is in a sameMinor version group\n  /// - ✘ One or more other instances have a different MAJOR version\n  /// - ? Crossing a major version boundary is unsafe\n  /// - ? We cannot know which MAJOR the user wants and have to ask them\n  SameMinorHasMajorMismatch,\n}\n```\n\nThe doc comment is the source of truth for when this state is assigned. Be\nprecise — every bullet should be independently verifiable from the visitor code.\n\n---\n\n## Step 3: Check whether any `impl` blocks need updating\n\nMost additions require no changes to `impl InstanceState`. The severity, the\n`is_fixable()` / `is_unfixable()` helpers, and the `get_name()` / `get_status_type()`\nmethods all work by matching on the outer category (`Valid`, `Invalid`,\n`Suspect`), not on individual variants.\n\nThe only time you need to touch `impl InstanceState` is if you are adding a\nvariant that needs a **custom severity** (rare) or a new top-level predicate\n(e.g. `is_banned()`). For ordinary additions, no changes are needed.\n\n---\n\n## Step 4: Write the detection logic in the visitor\n\nVisitor files live in `src/visit_packages/`. Each visitor receives a\n`&Dependency` and must call one of the mark methods on every instance:\n\n```rust\ninstance.mark_valid(ValidInstance::SatisfiesSameMinorGroup, specifier);\ninstance.mark_fixable(FixableInstance::DiffersToPin, expected_specifier);\ninstance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\ninstance.mark_conflict(SemverGroupAndVersionConflict::MatchConflictsWithLocal);\ninstance.mark_suspect(SuspectInstance::InvalidLocalVersion);\n```\n\nNever assign to `instance.state` directly — always use these methods.\n\nThe specifier argument to `mark_valid` and `mark_fixable` is the **expected**\nspecifier — what the instance should be set to. Read the actual specifier from\n`instance.descriptor.specifier`.\n\nExample — marking all instances when a major mismatch is detected (computed\nonce before the per-instance loop):\n\n```rust\nlet has_major_mismatch = dependency\n    .instances\n    .iter()\n    .any(|i| !i.already_has_same_major_as_all(&dependency.instances));\n\ndependency.instances.iter().for_each(|instance| {\n    let actual_specifier = &instance.descriptor.specifier;\n\n    if has_major_mismatch {\n        instance.mark_unfixable(UnfixableInstance::SameMinorHasMajorMismatch);\n        return;\n    }\n\n    // ... rest of visitor logic ...\n});\n```\n\nKey rules:\n\n- **Every instance must be marked** before the visitor returns. No instance may\n  remain in `InstanceState::Unknown` after `visit_packages()` completes.\n- **Compute per-dependency values once** before the loop (e.g. highest\n  specifier, major-mismatch boolean) to avoid O(n²) re-checking.\n- **Do not read `instance.state` inside the visitor** to decide what to do —\n  states are write-once during inspection.\n\n---\n\n## Step 5: Integrate into `visit_packages.rs` (only for new visitors)\n\nIf you are adding a variant to an **existing** visitor, skip this step.\n\nIf you are adding a **new visitor module**, register it in\n`src/visit_packages.rs`:\n\n```rust\nmod banned;\nmod ignored;\nmod indent;\nmod pinned;\nmod preferred_semver;\nmod same_minor;   // ← example of an existing entry\nmod same_range;\nmod snapped_to;\nmod your_new_visitor;  // ← add here\n\npub fn visit_packages(ctx: Context) -> Context {\n    ctx.version_groups.iter().sorted_by(order_snapped_to_groups_last).for_each(|group| {\n        group.dependencies.values().for_each(|dependency| match dependency.variant {\n            VersionGroupVariant::Banned => banned::visit(dependency),\n            VersionGroupVariant::SameMinor => same_minor::visit(dependency),\n            // ... add your new variant arm here if a new VersionGroupVariant was added ...\n            VersionGroupVariant::SnappedTo => snapped_to::visit(dependency, &ctx),\n        });\n    });\n    ctx\n}\n```\n\n---\n\n## Step 6: Write tests\n\nTest files live alongside their visitor: `src/visit_packages/same_minor_test.rs`\nis declared inside `same_minor.rs` with:\n\n```rust\n#[cfg(test)]\n#[path = \"same_minor_test.rs\"]\nmod same_minor_test;\n```\n\nTests use `TestBuilder` and `expect(&ctx).to_have_instances(...)`. Every\n`ExpectedInstance` must specify:\n\n- `state` — the exact `InstanceState` expected\n- `dependency_name` — the dependency name (not the instance id)\n- `id` — `\"{dep} in {location} of {package}\"` format\n- `actual` — the on-disk specifier string\n- `expected` — `Some(\"...\")` for the expected/fix-target specifier, or `None`\n- `overridden` — `Some(\"...\")` when a semver group preference was overridden\n\n```rust\nuse {\n    crate::{\n        instance_state::{FixableInstance::*, InstanceState, UnfixableInstance::*, ValidInstance::*},\n        test::{\n            builder::TestBuilder,\n            expect::{expect, ExpectedInstance},\n        },\n    },\n    serde_json::json,\n};\n\n#[test]\nfn major_mismatch_marks_all_instances_unfixable() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![json!({\n            \"name\": \"my-project\",\n            \"version\": \"1.0.0\",\n            \"dependencies\": {\n                \"foo\": \"1.2.0\"\n            },\n            \"devDependencies\": {\n                \"foo\": \"2.1.0\"\n            }\n        })])\n        .with_version_groups(vec![json!({\n            \"dependencies\": [\"foo\"],\n            \"policy\": \"sameMinor\"\n        })])\n        .build_and_visit_packages();\n\n    expect(&ctx).to_have_instances(vec![\n        ExpectedInstance {\n            state: InstanceState::valid(IsLocalAndValid),\n            dependency_name: \"my-project\",\n            id: \"my-project in /version of my-project\",\n            actual: \"1.0.0\",\n            expected: Some(\"1.0.0\"),\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /dependencies of my-project\",\n            actual: \"1.2.0\",\n            expected: None,\n            overridden: None,\n        },\n        ExpectedInstance {\n            state: InstanceState::unfixable(SameMinorHasMajorMismatch),\n            dependency_name: \"foo\",\n            id: \"foo in /devDependencies of my-project\",\n            actual: \"2.1.0\",\n            expected: None,\n            overridden: None,\n        },\n    ]);\n}\n```\n\nCover every leaf of the decision tree. For a visitor with semver group\ninteraction, the minimum test matrix is:\n\n| scenario                             | test name pattern                                          |\n| ------------------------------------ | ---------------------------------------------------------- |\n| no semver group, correct version     | `satisfies_..._and_has_no_semver_group`                    |\n| no semver group, wrong version       | `has_..._mismatch_and_no_semver_group`                     |\n| safe semver group, already matches   | `satisfies_..._and_matches_compatible_..._semver_group`    |\n| safe semver group, does not match    | `satisfies_..._but_mismatches_compatible_..._semver_group` |\n| unsafe semver group, already matches | `..._but_range_matches_incompatible_..._semver_group`      |\n| unsafe semver group, does not match  | `..._but_range_mismatches_incompatible_..._semver_group`   |\n\n---\n\n## Step 7: Verify commands need no changes\n\nCommands (`src/commands/*.rs`) operate on states generically:\n\n```rust\n// lint.rs, fix.rs etc. already handle your new variant automatically\ndependency.get_sorted_instances()\n    .filter(|instance| instance.is_invalid())  // or is_fixable(), is_unfixable() etc.\n    .for_each(|instance| { ... });\n```\n\nNew variants within existing categories (`Fixable`, `Unfixable`, etc.) are\npicked up automatically. You only need to touch command files if your new state\nrequires **bespoke fix logic** — for example, `IsBanned` requires removing the\ndependency entry rather than updating a version string.\n\n---\n\n## Step 8: Run the tests\n\n```bash\n# Run tests for a specific visitor\ncargo test same_minor\n\n# Run all tests\njust test\n```\n\n---\n\n## Checklist\n\n- [ ] Variant added to the correct enum in `src/instance_state.rs`\n- [ ] Doc comment uses `✓` `✘` `?` `!` bullets and describes every condition\n- [ ] Visitor uses `instance.mark_*()` methods — no direct state assignment\n- [ ] Per-dependency values computed once before the per-instance loop\n- [ ] Every instance is marked on every code path through the visitor\n- [ ] Tests cover every leaf of the decision tree\n- [ ] Tests use `expect(&ctx).to_have_instances(...)` with `ExpectedInstance`\n- [ ] No command changes needed (or bespoke fix logic added if required)\n\n---\n\n## Related files\n\n- `src/instance_state.rs` — all state variant definitions and annotations\n- `src/visit_packages.rs` — visitor dispatch\n- `src/visit_packages/*.rs` — visitor implementations\n- `src/visit_packages/*_test.rs` — test examples to copy patterns from\n- `src/commands/fix.rs` — example of how states drive fix behaviour\n- `.notes/reference/version-group-implementation-rules.md` — cross-cutting rules for all visitors\n\n<!-- File: .claude/skills/add-feature/SKILL.md (Tokens: ~494 | Category: anthropic-skill) -->\n---\nname: add-feature\ndescription: Add new features to Syncpack including commands and validation logic. Use when implementing new CLI commands, adding InstanceState variants, or extending version group behaviour.\n---\n\n# Add Feature\n\nGuide for adding new functionality to Syncpack.\n\n## Quick Decision\n\nWhat are you adding?\n\n- **New CLI command** (lint, fix, format, etc.) → See [adding-command.md](adding-command.md)\n- **New validation logic** (InstanceState variant) → See [adding-instance-state.md](adding-instance-state.md)\n- **Both** → Start with instance state, then add command\n\n## Prerequisites\n\nBefore adding any feature:\n\n1. Understand the 3-phase pattern: Create Context → Inspect Context → Run Command\n2. Know which phase your feature affects\n3. Check if similar code exists (use `ast-grep -p 'PATTERN' src/`)\n\n## Feature Types\n\n### Commands\n\nCommands are user-facing operations (`syncpack lint`, `syncpack fix`, etc.).\n\n**When to create a new command:**\n\n- New user-facing operation that doesn't fit existing commands\n- Different output format or behaviour needed\n\n**Registration points (all three required):**\n\n1. `src/cli.rs` - Add to `Subcommand` enum\n2. `src/main.rs` - Add match arm for dispatch\n3. `src/commands/*.rs` - Implement `pub fn run(ctx: Context) -> i32`\n\n→ Full guide: [adding-command.md](adding-command.md)\n\n### Instance States\n\nInstanceState variants describe validation results (valid, fixable, unfixable, suspect).\n\n**When to add a new state:**\n\n- New validation rule needed\n- New type of error/warning to report\n- New auto-fix capability\n\n**Location:** `src/instance_state.rs` + `src/visit_packages/*.rs`\n\n→ Full guide: [adding-instance-state.md](adding-instance-state.md)\n\n## Common Workflow\n\n1. Write failing test using TestBuilder\n2. Implement minimal code to pass\n3. Run `cargo clippy` and fix warnings\n4. Refactor if needed\n5. Update documentation\n\n## Checklist\n\nBefore submitting:\n\n- [ ] Tests pass (`just test`)\n- [ ] Zero clippy warnings\n- [ ] Follows existing patterns\n- [ ] Registered in all required places (for commands)\n- [ ] Uses TDD approach\n\n<!-- File: .claude/skills/document-code/SKILL.md (Tokens: ~1222 | Category: anthropic-skill) -->\n---\nname: document-code\ndescription: Add and update the documentation website for Syncpack. Use when making user-facing changes to the codebase.\n---\n\n# Document Code\n\nGuide for adding and updating Syncpack's documentation website.\n\n## Quick Decision\n\nWhat are you documenting?\n\n| Adding/Updating...    | Location                                         |\n| --------------------- | ------------------------------------------------ |\n| Command docs          | `site/src/content/docs/command/[cmd].mdx`        |\n| Config property       | `site/src/content/docs/config/[prop].mdx`        |\n| Version group variant | `site/src/content/docs/version-groups/[var].mdx` |\n| Semver group variant  | `site/src/content/docs/semver-groups/[var].mdx`  |\n| Status code           | `site/src/content/docs/status/[code].mdx`        |\n| FAQ                   | `site/src/faq/[id].mdx`                          |\n| Shared option partial | `site/src/_partials/option/[opt].mdx`            |\n| Reusable component    | `site/src/components/[name].astro`               |\n\n## Workflow\n\n1. **Read source** — `cli.rs` for commands, `rcfile.rs` for config, `instance_state.rs` for status codes\n2. **Create/update MDX** — Match existing structure exactly\n3. **Add link identifier** — Update `globalReferenceLinks` in `astro.config.mjs`\n4. **Test locally** — `pnpm run dev` from `site/`\n5. **Verify links** — Check all `(IDENTIFIER)` links resolve\n\n## Source to Docs Mapping\n\n| Source                  | Documentation                                         |\n| ----------------------- | ----------------------------------------------------- |\n| `src/cli.rs` Subcommand | `site/src/content/docs/command/*.mdx`                 |\n| `src/rcfile.rs` Rcfile  | `site/src/content/docs/config/*.mdx`                  |\n| `src/instance_state.rs` | `site/src/content/docs/status/*.mdx`                  |\n| `src/specifier.rs`      | `site/src/content/docs/reference/specifier-types.mdx` |\n\n## Local Development\n\n```bash\ncd site\npnpm run dev          # Start at http://localhost:4321/syncpack\npnpm run build        # Verify build succeeds\n```\n\n## Site Overview\n\n- Source located at `site/src/`\n- Built with [Starlight](https://starlight.astro.build/) on top of [Astro](https://astro.build/)\n- Content at `site/src/content/docs/**/*.mdx`\n- Reusable partials at `site/src/_partials/**/*.mdx`\n- Reusable components at `site/src/components/*.astro`\n\n## Command Documentation\n\nEach command has a corresponding file at `site/src/content/docs/command/[command].mdx`.\n\n### Required Structure (in order)\n\n1. Description of the command's purpose from end user perspective\n2. `## Examples` — Single bash code block with commented examples\n3. `## Options` — Start with `<QuoteFilters />` callout, then `###` for each option alphabetically (`--help` last)\n\n### Shared Option Partials\n\nMany commands share options. Define reusable partials at `site/src/_partials/option/*.mdx`:\n\n```mdx\nimport ConfigOption from \"@partials/option/config.mdx\";\n\n<ConfigOption command=\"update\" />\n```\n\n## Config Documentation\n\nEach config property has a file at `site/src/content/docs/config/[property].mdx`.\n\n### Required Structure\n\n1. Description of the config's purpose from end user perspective\n2. `## Default Value` — Code block showing default (when applicable)\n3. `## Examples` — Code blocks with commented examples\n\n### Version Groups and Semver Groups\n\nEach variant has its own page:\n\n- `site/src/content/docs/version-groups/*.mdx`\n- `site/src/content/docs/semver-groups/*.mdx`\n\n#### Required Structure\n\n1. Description of the group's purpose from end user perspective\n2. `## Examples` — Code block per example, capture novel use cases\n3. `## Configuration` — `###` for each config property\n\n## Status Code Documentation\n\nEach status code has a file at `site/src/content/docs/status/[code].mdx`.\n\nReference page at `site/src/content/docs/reference/status-codes.mdx` explains the `InstanceState` enum and its nested enums (`ValidInstance`, `InvalidInstance`, `SuspectInstance`).\n\n## FAQ Documentation\n\n- Create FAQ files at `site/src/faq/[id].mdx`\n- Listed at `/syncpack/faq` via `site/src/pages/faq.astro`\n- Individual pages via `site/src/pages/faq/[id].astro`\n\n## Linking Between Pages\n\nUse named identifiers in markdown links:\n\n```mdx\nWhen using the [update](COMMAND_UPDATE) command\n```\n\nDefine identifiers in `globalReferenceLinks` function in `site/astro.config.mjs`.\n\n## Open Graph Images\n\nGenerated by `site/src/pages/og/[...path].png.ts` using [Satori](https://github.com/vercel/satori).\n\n## Troubleshooting\n\n| Problem                       | Fix                                           |\n| ----------------------------- | --------------------------------------------- |\n| Link not working              | Check identifier in `globalReferenceLinks`    |\n| Partial not rendering         | Verify import path uses `@partials/` alias    |\n| OG image broken               | Check `og/[...path].png.ts` handles new route |\n| Build fails                   | Run `pnpm run build` locally to see error     |\n| Page not appearing in sidebar | Check frontmatter and Starlight config        |\n\n## Checklist\n\nBefore submitting documentation:\n\n- [ ] Structure matches existing pages of same type\n- [ ] Link identifier added to `astro.config.mjs`\n- [ ] Local dev server renders correctly\n- [ ] Build succeeds (`pnpm run build`)\n- [ ] All internal links resolve\n\n<!-- File: .claude/skills/fix-bug/SKILL.md (Tokens: ~1011 | Category: anthropic-skill) -->\n---\nname: fix-bug\ndescription: Debug and fix bugs in Syncpack using scientific debugging methodology. Use when a test is failing, unexpected behaviour occurs, or investigating issues. Covers hypothesis-driven debugging and TDD-based fixes.\n---\n\n# Fix Bug\n\nGuide for debugging and fixing bugs in Syncpack.\n\n## Scientific Debugging Workflow\n\n1. **Observe** — Understand the symptom precisely\n2. **Hypothesise** — Form a theory about the root cause\n3. **Experiment** — Test the hypothesis with targeted changes\n4. **Validate** — Confirm the fix with tests\n5. **Verify** — Run full test suite\n\n## Step 1: Observe\n\nGather information before changing code:\n\n```bash\n# Run the failing test with output\ncargo test test_name -- --nocapture\n\n# Run with backtrace\nRUST_BACKTRACE=1 cargo test test_name\n\n# Test against fixture\ncd fixtures/fluid-framework\ncargo run -- lint\n```\n\nQuestions to answer:\n\n- What is the exact error message?\n- What input triggers it?\n- What is the expected vs actual behaviour?\n\n## Step 2: Hypothesise\n\nCommon root causes by symptom:\n\n| Symptom              | Likely Cause                                            |\n| -------------------- | ------------------------------------------------------- |\n| State is `Unknown`   | `visit_packages` not called or instance skipped         |\n| Wrong state assigned | Validation logic order, earlier check overriding        |\n| Instance not found   | Package/dependency name mismatch, location format       |\n| Command not running  | Missing registration in cli.rs, main.rs, or commands.rs |\n| Panic/unwrap failure | Unexpected None or Err value                            |\n\n## Step 3: Experiment\n\nAdd targeted debug output:\n\n```rust\nuse log::debug;\n\ndebug!(\"Instance: {:#?}\", instance);\ndebug!(\"State before: {:?}\", instance.state.borrow());\n```\n\nSearch for related code:\n\n```bash\n# Find where state is assigned\nast-grep -p 'InstanceState::fixable' src/visit_packages/\n\n# Find similar patterns\nast-grep -p 'PATTERN' src/\n```\n\n## Step 4: Validate\n\nWrite a failing test FIRST (TDD):\n\n```rust\n#[test]\nfn reproduces_the_bug() {\n    let ctx = TestBuilder::new()\n        .with_packages(vec![/* minimal reproduction */])\n        .build_and_visit_packages();\n\n    // Assert expected behaviour (will fail initially)\n}\n```\n\nThen fix the code until the test passes.\n\n## Step 5: Verify\n\n```bash\njust test        # All tests pass\ncargo clippy     # No warnings\n```\n\n## Where to Look\n\n### By component\n\n| Issue with...    | Check...                                 |\n| ---------------- | ---------------------------------------- |\n| CLI parsing      | `src/cli.rs`                             |\n| Config loading   | `src/config.rs`, `src/rcfile.rs`         |\n| Package reading  | `src/packages.rs`, `src/package_json.rs` |\n| State assignment | `src/visit_packages/*.rs`                |\n| Command output   | `src/commands/*.rs`, `src/commands/ui/`  |\n| Version parsing  | `src/specifier/*.rs`                     |\n\n### By phase\n\n| Phase   | What happens                             | Files                                      |\n| ------- | ---------------------------------------- | ------------------------------------------ |\n| Create  | Read config, packages, collect instances | `context.rs`, `packages.rs`                |\n| Inspect | Assign InstanceState                     | `visit_packages.rs`, `visit_packages/*.rs` |\n| Run     | Process instances, output/write          | `commands/*.rs`                            |\n\n## Common Fixes\n\n### Instance state not being assigned\n\nCheck:\n\n1. Is the instance's version group variant handled in `visit_packages.rs`?\n2. Is there an early return skipping this instance?\n3. Is another validation assigning a state first?\n\n### Test assertion failing\n\nCheck:\n\n1. Is `build_and_visit_packages()` called (not just `build()`)?\n2. Is the location string format correct? (`\"{dep} in {location} of {package}\"`)\n3. Are there multiple instances of the same dependency?\n\n### Command not found\n\nVerify registration in all three places:\n\n1. `src/cli.rs` — `Subcommand` enum\n2. `src/main.rs` — match arm\n3. `src/commands.rs` — module declaration\n\n## Debugging Tips\n\n- **Isolate** — Create minimal reproduction\n- **Binary search** — Comment out code to narrow down\n- **Print early** — Add debug output before suspected area\n- **Check order** — Validation order matters (first match wins)\n- **Read tests** — Existing tests show expected behaviour\n\n## Anti-Patterns\n\n❌ Changing code without understanding the cause\n❌ Removing code just to make tests pass\n❌ Fixing symptoms instead of root cause\n❌ Skipping the test-first step\n\n<!-- File: .claude/skills/front-loading/SKILL.md (Tokens: ~1683 | Category: anthropic-skill) -->\n---\nname: front-loading\ndescription: Put the answer first, then context and details follow. Use when writing documentation, tutorials, error messages, or any communication where readers need to decide quickly if something is relevant to them.\n---\n\n# Front-Loading\n\nPut the answer first. Context follows.\n\n## What This Is\n\nFront-loading means leading with the most important information. Users decide in the first few words whether to keep reading.\n\nStructure your communication as:\n\n1. **The point** (first sentence)\n2. **Why it matters** (next 1–2 sentences)\n3. **Details and evidence** (rest of content)\n\nMost writers do the opposite: context first, then conclusion. Readers get lost in the journey before reaching the destination.\n\n## When to Use\n\n- Writing documentation or guides\n- Structuring arguments or explanations\n- Creating tutorial sections\n- Writing error messages\n- Composing email or feedback\n- Organizing any sequential information\n\nFront-load at every level: sentences, paragraphs, sections, documents.\n\n## How to Apply\n\n### 1. Lead with the Conclusion\n\nPut the answer first. Don't make readers wait.\n\n**Bad (context first):**\n\"There are many different ways to approach testing. Some teams test before writing code. Others test after. Some tests run quickly, others take longer. The most important thing is consistency. So you should write tests.\"\n\n**Good (conclusion first):**\n\"Write tests. Consistency matters more than approach.\"\n\nThen explain the trade-offs if needed.\n\n### 2. Answer the Reader's Question Immediately\n\nReaders have a question in mind. Answer it in the first sentence.\n\n**Bad (makes reader wait):**\n\"Syncpack is a CLI tool built in Rust. It uses a configuration file in JSON format. The configuration file specifies rules for how to synchronise dependencies. These rules can be customized per workspace...\"\n\nReader's question: \"How do I configure Syncpack?\" Buried in paragraph 3.\n\n**Good (answers immediately):**\n\"Configure Syncpack in `.syncpackrc.json` at your monorepo root. Specify version groups and rules for each.\"\n\nThen explain each option.\n\n### 3. Use Signposting\n\nTell readers what's coming before they read it.\n\n**Bad (no preview):**\n\"First, install Node 18. Then, verify the installation. Next, clone the repository. After that, install dependencies...\"\n\nReader: \"Where is this going?\"\n\n**Good (preview structure):**\n\"Three steps: Install Node, clone the repo, and run `npm install`.\"\n\nThen detail each step.\n\n### 4. When Explaining a Process, Show the End State First\n\nLet readers know what success looks like before they start.\n\n**Bad (journey without destination):**\n\"Follow these 12 steps to set up CI/CD. Step 1: Create a workflow file. Step 2: Add a trigger. Step 3: Define a job...\"\n\nReader: \"Why am I doing this?\"\n\n**Good (end state first):**\n\"Goal: Your PR automatically runs tests and fails if any break. Here's how to set it up in 12 steps.\"\n\nThen detail steps 1–12.\n\n### 5. Structure Paragraphs with Topic Sentence First\n\nEach paragraph's first sentence should be its main idea.\n\n**Bad (idea buried):**\n\"There are many different caching strategies. Some store values in memory. Others use disk. Still others use remote services. The strategy you choose depends on your use case. For most applications, in-memory caching is sufficient.\"\n\nReader has to parse the whole paragraph to find the point.\n\n**Good (topic first):**\n\"For most applications, in-memory caching is sufficient. Other strategies (disk, remote) exist for specific cases. Choose based on your performance and persistence needs.\"\n\n### 6. When Providing Options, Lead with the Recommendation\n\nDon't make readers compare three options to find the best one.\n\n**Bad (no guidance):**\n\"You can use approach A, B, or C. Each has trade-offs. A is simple but inflexible. B is flexible but complex. C is somewhere in between. Most projects use B or C.\"\n\nReader: \"So which should I use?\"\n\n**Good (recommendation first):**\n\"Use approach B (flexible but complex). It scales as your needs grow. Use approach A only if simplicity is critical. Approach C rarely adds value.\"\n\n## Examples\n\n### Documentation Section\n\n**Bad (context first):**\n\"The registry client handles communication with npm. Over time, performance became an issue. We realized the client was making unnecessary requests. After analysis, we found that caching solved the problem. So the registry client now caches responses.\"\n\n**Good (point first):**\n\"The registry client caches npm responses to improve performance. This reduced request overhead by 60%.\"\n\nThen explain the details.\n\n---\n\n### Error Message\n\n**Bad (generic context):**\n\"An operation has encountered an error condition. The system attempted to perform a task. This task requires certain prerequisites. One or more prerequisites were not met. Please verify your setup.\"\n\n**Good (specific answer):**\n\"Missing required field in config: 'versionGroups'. See `.syncpackrc.json` example.\"\n\n---\n\n### Tutorial Section\n\n**Bad (setup before goal):**\n\"Before you can run Syncpack, you need to set up your monorepo structure. This involves creating a root directory. Inside this directory, you need workspace packages. Each package should have a `package.json`. Once you have this structure, you can run Syncpack.\"\n\n**Good (goal first):**\n\"Goal: Run Syncpack to sync versions across your monorepo.\"\n\nThen list the prerequisites (monorepo structure, workspace packages).\n\n---\n\n### Workflow Documentation\n\n**Bad (steps without outcome):**\n\"Step 1: Write your implementation. Step 2: Write tests. Step 3: Run tests locally. Step 4: Push to GitHub. Step 5: Wait for CI. Step 6: Address failures. Step 7: Merge when green.\"\n\n**Good (outcome first):**\n\"Goal: Merge code only when tests pass and CI approves.\"\n\nThen list the steps to reach that goal.\n\n## Trade-offs\n\n### When NOT to front-load\n\n- **Narrative/storytelling:** Sometimes the journey is the point. Build suspense if appropriate.\n- **Teaching from first principles:** Some explanation is necessary before jumping to conclusions.\n- **Building trust:** Sometimes context _is_ the point (e.g., \"here's why we made this decision\").\n- **Sensitive feedback:** Sometimes leading with criticism is harmful. Lead with understanding instead.\n\nThe principle: **Match the reader's mental model.** If they need context to understand the conclusion, provide context first.\n\n### Common mistake\n\nOver-front-loading: \"Use this. End of story.\" without any reasoning alienates readers who need understanding.\n\nSolve by asking: \"Will the reader understand why this conclusion is right?\" If no, provide enough context. But lead with the conclusion, not the context.\n\n## Testing\n\nHow to know if you've front-loaded effectively:\n\n1. **First sentence test:** Can someone read only the first sentence and understand the main point?\n2. **Skim test:** Someone skims headings and bullets—do they get the gist?\n3. **Reader expectation test:** \"What happens next?\" readers predict after each section. Were they right?\n4. **Reduction test:** Remove everything after the first paragraph. Is the main point clear? (If yes, you front-loaded successfully.)\n5. **Fresh eyes test:** Have someone read the first paragraph and summarize. Did they get it right?\n\n## Application in Syncpack\n\nFront-loading improves:\n\n- **Skill descriptions:** Users know instantly if it's relevant\n- **Error messages:** Users understand what failed and how to fix it\n- **Documentation sections:** Readers find what they need faster\n- **Code comments:** Maintainers understand the \"why\" before the \"how\"\n- **Commit messages:** Reviewers know the intent before reading code\n- **Proposals or plans:** Stakeholders see the recommendation before the analysis\n\nEvery document you write is a race against the reader's attention span. Front-load to win that race.\n\n<!-- File: .claude/skills/optimize-benchmarks/SKILL.md (Tokens: ~1590 | Category: anthropic-skill) -->\n---\nname: optimize-benchmarks\ndescription: Iterative performance optimisation loop for syncpack-specifier. Runs benchmarks, identifies bottlenecks, applies optimisations, verifies tests pass and benchmarks improve, then repeats.\n---\n\n# Optimize Benchmarks\n\nIterative loop: benchmark, optimise, test, verify improvement, repeat.\n\n## Workflow\n\n### 1. Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --save-baseline before 2>&1 | tail -40\n```\n\nSave the output. Identify which variants are slowest.\n\n### 2. Identify Bottleneck\n\nFrom the full baseline, focus on the **slowest** benchmarks first.\n\nPriority order for specifier parsing:\n\n1. `Specifier::create` — the main parse function, called for every version string\n2. `parser::is_range` — checks 12 regexes sequentially\n3. `parser::is_exact` — checks 4 regexes sequentially\n4. `parser::is_complex_range` — splits, collects, iterates\n5. Individual regex matches in `regexes.rs`\n\n### 3. Apply ONE Optimisation\n\nMake a **single, focused change**. Do NOT bundle multiple optimisations — each must be independently measurable.\n\n### 4. Verify Tests Pass\n\n```bash\ncargo test -p syncpack-specifier 2>&1 | tail -5\n```\n\nIf tests fail, fix or revert. Never proceed with failing tests.\n\n### 5. Benchmark Against Baseline\n\n```bash\ncargo bench -p syncpack-specifier -- --baseline before 2>&1 | tail -40\n```\n\nLook for `[-XX.XXX% ...]` (improvement) or `[+XX.XXX% ...]` (regression).\n\n### 6. Evaluate\n\n- **Improved:** Report the gains. Update baseline: `cargo bench -p syncpack-specifier -- --save-baseline before`. Continue to step 2.\n- **No change:** Revert and try a different approach.\n- **Regressed:** Revert immediately.\n\n### 7. Repeat\n\nGo to step 2. Stop when:\n\n- User says stop\n- No bottlenecks remain\n- Gains are <1% across all benchmarks\n\n## Known Optimisation Opportunities\n\n### High Impact\n\n**Replace regex with char-based parsing in `parser.rs` / `regexes.rs`**\n\nMost regexes in `regexes.rs` match simple patterns like `^[0-9]+\\.[0-9]+\\.[0-9]+$` (exact semver). These can be replaced with byte/char iteration:\n\n```rust\n// Instead of regex EXACT: r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\"\nfn is_exact_version(s: &str) -> bool {\n  let mut dots = 0;\n  let bytes = s.as_bytes();\n  if bytes.is_empty() { return false; }\n  for &b in bytes {\n    match b {\n      b'0'..=b'9' => {},\n      b'.' => dots += 1,\n      _ => return false,\n    }\n  }\n  dots == 2\n}\n```\n\nRegex `is_match()` has overhead even for simple patterns: engine setup, capture group allocation. Char-based parsing for these patterns is 5-20x faster.\n\n**Reduce sequential regex attempts in `parser::is_range`**\n\n`is_range` tries 12 regexes. Instead, match on first char(s) to dispatch:\n\n```rust\nfn is_range(s: &str) -> bool {\n  match s.as_bytes().first() {\n    Some(b'^') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'~') => is_semver_after(s, 1) || is_semver_tag_after(s, 1),\n    Some(b'>') => { /* check >= vs > then validate remainder */ },\n    Some(b'<') => { /* check <= vs < then validate remainder */ },\n    _ => false,\n  }\n}\n```\n\n**Consolidate related regex patterns**\n\nMany regexes are pairs: `EXACT` + `EXACT_TAG`, `CARET` + `CARET_TAG`, etc. Merge each pair into one function that handles both cases:\n\n```rust\nfn is_exact(s: &str) -> bool {\n  // Parse digits.digits.digits, then optionally -tag\n  let rest = parse_semver_triple(s)?;\n  rest.is_empty() || rest.starts_with('-')\n}\n```\n\n### Medium Impact\n\n**Replace `lazy_static` with `std::sync::OnceLock`**\n\n`lazy_static` uses an extra indirection layer. `OnceLock` (stable since Rust 1.80) is zero-cost after init:\n\n```rust\nuse std::sync::OnceLock;\n\nfn exact_regex() -> &'static Regex {\n  static RE: OnceLock<Regex> = OnceLock::new();\n  RE.get_or_init(|| Regex::new(r\"^[0-9]+\\.[0-9]+\\.[0-9]+$\").unwrap())\n}\n```\n\nBut if regex is being replaced with char-based parsing, this becomes irrelevant.\n\n**Reorder checks in `Specifier::create` by frequency**\n\nIn a typical monorepo, most specifiers are `^x.y.z` (range) or `x.y.z` (exact). The current order already checks exact first, then range — good. But `is_exact` tries 4 regex patterns. A single fast char check can short-circuit:\n\n```rust\n// Fast path: first char is digit → likely exact or major or minor\n// Fast path: first char is ^ or ~ → likely range\n```\n\n**Avoid `String` allocation in `strip_semver_range`**\n\n`strip_semver_range` returns `&str` (already good), but callers like `Range::create` then `.to_string()` the result. Consider whether the allocation can be deferred.\n\n### Low Impact\n\n- Replace `HashMap` in caches with `FxHashMap` (faster hashing for short strings)\n- Use `SmallString` or stack-allocated strings for short specifiers\n- Pre-size cache `HashMap` with expected capacity\n\n## Architecture Notes\n\nKey files in `crates/syncpack-specifier/src/`:\n\n| File                         | Role                                                        |\n| ---------------------------- | ----------------------------------------------------------- |\n| `lib.rs`                     | `Specifier` enum, `create()` dispatch, caches               |\n| `parser.rs`                  | `is_exact()`, `is_range()`, etc. — classification functions |\n| `regexes.rs`                 | All `lazy_static` regex patterns                            |\n| `exact.rs`, `range.rs`, etc. | Variant constructors calling `node_semver`                  |\n| `semver_range.rs`            | `SemverRange` enum, `parse()`                               |\n\nThe hot path is: `Specifier::create()` → `parser::is_*()` → `regexes::*` → variant `::create()` → `node_semver` parsing.\n\nOptimising the `parser::is_*` layer gives the biggest wins because it runs for **every** specifier, and most of the time most checks return `false` (only one branch matches).\n\n## Rules\n\n- ONE change per iteration\n- Always verify tests pass before benchmarking\n- Always compare against baseline\n- Report numbers: `before → after (% change)`\n- Revert regressions immediately\n- Don't optimise what doesn't show up in benchmarks\n- Use **fast iteration** (`\"batch\"` filter) during the loop, **full suite** only at start and end\n\n<!-- File: .claude/skills/review-docs/SKILL.md (Tokens: ~479 | Category: anthropic-skill) -->\n---\nname: review-docs\ndescription: Review and improve Syncpack documentation for clarity, completeness, and consistency. Use when enhancing docs or validating before publication.\n---\n\n# Review Docs\n\nReview and improve Syncpack documentation against project standards.\n\n## When to Use\n\n- Validating docs before publication\n- Improving existing documentation\n- Ensuring consistency across pages\n- User reports documentation is unclear\n\n## Related Skills\n\nUse with:\n\n- `document-code` — Reference for structure and locations\n- `signal-over-noise` — Apply to remove filler and obvious explanations\n- `front-loading` — Ensure actionable information comes first\n\n## Your Task\n\nGiven a documentation path, review content against standards. Suggest concrete improvements.\n\n## Quality Checklist\n\n- [ ] **Clarity**: First line immediately shows what it covers\n- [ ] **Examples**: Practical, show real use cases, cover major patterns\n- [ ] **Completeness**: All relevant aspects documented\n- [ ] **Consistency**: Formatting matches other docs\n- [ ] **Conciseness**: No fluff; every sentence earns space\n- [ ] **Accessibility**: Users find what they need without deep reading\n- [ ] **Reuse**: Reference shared partials when available\n\n## Review Process\n\n1. Read the documentation file\n2. Cross-reference structure with similar docs\n3. Identify information gaps where users might get stuck\n4. Check for shared partials at `site/src/_partials/`\n5. Apply signal-over-noise: Can you cut 20% of words without losing meaning?\n\n## Review Output\n\nProvide structured feedback:\n\n**Current State**\n\n- Assessment of documentation quality and coverage\n\n**Issues Found**\n\n- Specific problems with locations (section names or line references)\n- Prioritize by impact: clarity > completeness > polish\n\n**Recommendations**\n\n- Concrete improvements with examples\n- Rewrite short sections if needed\n- Flag missing content or examples\n- Suggest shared partials to reuse\n\n**Priority Actions**\n\n- Top 2-3 changes to make immediately\n\n## Reference\n\n**Docs:** `site/src/content/docs/`\n**Shared partials:** `site/src/_partials/`\n**Source of truth:** `src/` (Rust implementation)\n**Local testing:** `cd site && pnpm run dev` then verify at `http://localhost:4321/syncpack`\n\n<!-- File: .claude/skills/search-code/SKILL.md (Tokens: ~604 | Category: anthropic-skill) -->\n---\nname: search-code\ndescription: Search for code patterns in Syncpack. Use when finding symbols, implementations, or understanding how code is used. Covers ast-grep for Rust and grep/rg for other cases.\n---\n\n# Search Code\n\nGuide for searching the Syncpack codebase effectively.\n\n## Golden Rule\n\n**Use `ast-grep` for Rust files** — it filters out comments, strings, and docs.\n\n## Quick Reference\n\n| Looking for...                    | Use         |\n| --------------------------------- | ----------- |\n| Rust symbols, patterns, structs   | `ast-grep`  |\n| Multiple file types               | `grep`/`rg` |\n| Content in comments intentionally | `grep`/`rg` |\n| Non-Rust files (JSON, MD, TOML)   | `grep`/`rg` |\n\n## ast-grep Examples\n\n```bash\n# Find struct definitions\nast-grep -p 'pub struct $NAME' src/\n\n# Find function calls\nast-grep -p 'Specifier::new' src/\n\n# Find enum variants\nast-grep -p 'pub enum $NAME' src/\n\n# Find impl blocks\nast-grep -p 'impl $TYPE' src/\n\n# Find specific method definitions\nast-grep -p 'pub fn run($$$) -> i32' src/\n\n# Find match expressions\nast-grep -p 'match $EXPR { $$$ }' src/\n```\n\n## Pattern Syntax\n\n| Pattern | Matches                                      |\n| ------- | -------------------------------------------- |\n| `$NAME` | Single identifier                            |\n| `$$$`   | Zero or more items (arguments, fields, etc.) |\n| `$_`    | Any single expression                        |\n\n## When to Use grep/rg\n\n```bash\n# Search across file types\nrg \"TODO\" --type rust --type md\n\n# Search in comments (ast-grep ignores these)\nrg \"FIXME\" src/\n\n# Search non-Rust files\nrg \"version\" Cargo.toml\n\n# Case-insensitive search\nrg -i \"error\" src/\n```\n\n## Common Searches\n\n### Find where a type is used\n\n```bash\nast-grep -p 'Context' src/\n```\n\n### Find all InstanceState assignments\n\n```bash\nast-grep -p 'InstanceState::valid' src/\nast-grep -p 'InstanceState::fixable' src/\nast-grep -p 'InstanceState::unfixable' src/\n```\n\n### Find test files\n\n```bash\nfind src -name '*_test.rs'\n```\n\n### Find command implementations\n\n```bash\nast-grep -p 'pub fn run($$$) -> i32' src/commands/\n```\n\n## Tips\n\n- Start broad, then narrow with more specific patterns\n- Use `ast-grep` output to find the file, then `read_file` to understand context\n- Chain searches: find the type, then find where it's constructed\n\n<!-- File: .claude/skills/signal-over-noise/SKILL.md (Tokens: ~1296 | Category: anthropic-skill) -->\n---\nname: signal-over-noise\ndescription: Maximize useful information per word by removing filler, obvious explanations, and hedging language. Use when writing documentation, error messages, code comments, or any communication where clarity and conciseness matter.\n---\n\n# Signal Over Noise\n\nMaximize useful information per word. Remove everything that doesn't earn its space.\n\n## What This Is\n\nSignal = information that moves the reader toward action or understanding.\nNoise = filler, explanations of obvious things, hedging language, unnecessary context.\n\nHigh signal-to-noise ratio means readers get what they need faster. Every word counts.\n\n## When to Use\n\n- Writing documentation, guides, skill descriptions\n- Creating error messages and feedback\n- Structuring arguments or explanations\n- Naming things (variables, functions, branches)\n- Any communication where clarity matters\n\n## How to Apply\n\n### 1. Remove Filler Words\n\n**High noise:**\n\n- \"basically\"\n- \"essentially\"\n- \"in order to\"\n- \"comprehensive\"\n- \"helpful\"\n- \"I think that\"\n- \"it could be argued that\"\n\n**Before:** \"Basically, you should essentially use this pattern in order to get comprehensive error handling.\"\n\n**After:** \"Use this pattern for error handling.\"\n\n### 2. Cut Explanations of Obvious Things\n\nDon't explain what readers already know. Assume competence.\n\n**Before:** \"Rust is a programming language. In Rust, you write code. This code runs on computers. When you compile Rust code, it produces a binary executable file.\"\n\n**After:** \"Compile Rust code to a binary with `cargo build --release`.\"\n\n### 3. Front-Load the Point\n\nPut the actionable information first. Context follows.\n\n**Before:** \"There are several reasons why you might want to consider testing your code. First, testing helps catch bugs. Second, it makes refactoring safer. Most importantly, tests serve as documentation. You should write tests.\"\n\n**After:** \"Write tests to catch bugs, enable safe refactoring, and document behavior.\"\n\n### 4. Use Active, Direct Language\n\n**High noise:** \"It is recommended that consideration be given to...\"\n\n**Signal:** \"Consider...\"\n\n**High noise:** \"The file was created by the user.\"\n\n**Signal:** \"Create the file.\"\n\n### 5. One Idea Per Sentence\n\nLong sentences hide the point. Break them.\n\n**Before:** \"When you're writing code that handles errors, which can happen in many places throughout your application, you should think about whether you want to handle them at the point where they occur or propagate them upward, and this decision affects how you structure your code.\"\n\n**After:** \"Handle errors where they occur or propagate them upward. This choice shapes your code structure. Decide early.\"\n\n### 6. Name Things Precisely\n\nNames are communication. Bad names add noise; good names clarify.\n\n**High noise:** `process_data()`, `handle_thing()`, `do_stuff()`\n\n**Signal:** `validate_user_input()`, `retry_failed_request()`, `parse_json_config()`\n\n### 7. Omit Context Readers Already Have\n\nDon't repeat what's obvious from surrounding text.\n\n**Before:** \"Here's an example of how to use the feature. In this example, we show you how to use it.\"\n\n**After:** \"Example:\"\n\n## Trade-offs\n\n### When NOT to apply aggressively\n\n- **Teaching beginners:** Some explanatory redundancy helps them build intuition\n- **Safety-critical systems:** Explicit redundancy catches misunderstanding\n- **Legal/compliance docs:** Specificity sometimes requires verbose phrasing\n- **User research notes:** Capture raw data, organize later\n\n### The balance\n\nSignal-over-noise isn't \"be terse.\" It's \"be useful.\" Sometimes that means:\n\n- Repeating a key phrase for emphasis\n- Providing context that prevents misreading\n- Showing your work so readers can verify reasoning\n- Acknowledging limitations explicitly\n\nRemove noise, keep signal. Don't remove useful context.\n\n## Examples\n\n### Code Documentation\n\n**Before:**\n\n```\nThis function is useful for validating email addresses.\nIt takes a string as input, which represents the email address\nthat you want to check. The function will return true if the\nemail is valid, or false if it is not valid. You might want to\nuse this function when you need to validate emails.\n```\n\n**After:**\n\n```\nValidate email addresses. Returns true if valid, false otherwise.\n```\n\n### Error Messages\n\n**Before:**\n\n```\nAn error has occurred in the system. The operation you attempted\ncould not be completed successfully. Please try again later or\ncontact support if the problem persists.\n```\n\n**After:**\n\n```\nFailed to fetch user data. Retry or contact support.\n```\n\n### Skill Description\n\n**Before:**\n\n```\nThis skill helps you think about how to communicate more effectively\nby using techniques that are designed to help you reduce unnecessary\ninformation and focus on the important parts of what you're trying\nto say so that your readers understand your message better and faster.\n```\n\n**After:**\n\n```\nMaximize useful information per word. Remove filler, obvious explanations, and hedging language. Every word should earn its space.\n```\n\n## Testing\n\nHow to know if you've applied this principle:\n\n1. **Read it aloud.** Does every sentence move you forward?\n2. **Ask \"why?\"** for each phrase. If the answer is \"readers already know this,\" cut it.\n3. **Count words.** Did you reduce the word count by 20–40%? Good sign.\n4. **Test comprehension.** Can a fresh reader understand it without re-reading?\n5. **Check names.** Are function/variable names predictable and specific?\n\n## Application in Syncpack\n\nThis principle strengthens all your other skills:\n\n- **write-code:** Function names, comments, commit messages all benefit\n- **document-code:** Docs communicate faster when noise is removed\n- **add-feature:** Feature descriptions and changelogs stay concise\n- **fix-bug:** Error messages become actionable, not explanatory\n- **search-code:** Clear naming makes code more discoverable\n\nApply signal-over-noise across the board. Your CLAUDE.md already mandates it—this skill formalizes how.\n\n<!-- File: .claude/skills/testing-gaps/SKILL.md (Tokens: ~762 | Category: anthropic-skill) -->\n---\nname: testing-gaps\ndescription: Run coverage, inspect results, and identify missing test scenarios for a given source file. Use when analysing test coverage or finding untested branches.\n---\n\n# Testing Gaps\n\nFind untested branches and missing real-world test scenarios for a source file.\n\n## Workflow\n\n1. **Run text coverage** for the target file\n2. **Identify uncovered branches** from the output\n3. **Cross-reference** with test file to understand what's tested\n4. **Report** uncovered branches + missing real-world scenarios\n\n## Step 1: Run Coverage\n\nUse `cargo llvm-cov` with `--text` output (not `--html`) for parseable results.\n\n```bash\n# Full coverage, filtered to target file\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/TARGET_FILE\\.rs:/,/^$/p'\n```\n\nExample for `preferred_semver.rs`:\n\n```bash\ncargo llvm-cov test --text \\\n  --ignore-run-fail \\\n  --ignore-filename-regex '(_test.rs|\\/test\\/)' \\\n  2>/dev/null | sed -n '/preferred_semver\\.rs:/,/^$/p'\n```\n\n## Step 2: Read Coverage Output\n\nThe text format shows per-line execution counts:\n\n```\n  42|     80|        .and_then(|range| ...)     # Hit 80 times\n  74|      0|        Some(preferred)            # Never hit\n 321|      0|        instance.mark_conflict(...) # Never hit\n```\n\n- `count > 0` = covered\n- `count = 0` = uncovered branch\n- `^N` annotations on sub-expressions show partial coverage within a line\n\n### Extract only uncovered lines\n\n```bash\n... | grep -E '^\\s+\\d+\\|\\s+0\\|'\n```\n\n## Step 3: Classify Gaps\n\nFor each uncovered branch, determine:\n\n1. **What code path leads here?** — Trace the `if/else` chain backward\n2. **What input would trigger it?** — What package.json + config combination\n3. **Is it reachable?** — Some branches may be defensive/unreachable\n4. **Is it worth testing?** — Real-world scenario vs theoretical edge case\n\n### Categories\n\n| Category                         | Action            |\n| -------------------------------- | ----------------- |\n| Real-world scenario never tested | Write a test      |\n| Edge case in existing logic      | Write a test      |\n| Defensive branch (unreachable)   | Note but skip     |\n| Dead code                        | Consider removing |\n\n## Step 4: Identify Missing Scenarios\n\nBeyond line coverage, look for missing _combinations_:\n\n- Feature A tested, Feature B tested, A+B never tested together\n- Only tested with one package, never with multiple\n- Only tested with `highestSemver`, never `lowestSemver`\n- Only tested with one range type (`^`), never others (`>=`, `<=`)\n- Only tested without semver groups, never with\n\n## Step 5: Report\n\nStructure findings as:\n\n1. **Uncovered branches** — Specific lines, what triggers them, why untested\n2. **Missing real-world scenarios** — Combinations and interactions not covered\n\n## Running Tests After Adding Coverage\n\n```bash\njust test                              # All tests\ncargo test test_name -- --nocapture   # Specific test\njust coverage                         # Regenerate coverage report\n```\n\n<!-- File: .claude/skills/write-code/SKILL.md (Tokens: ~919 | Category: anthropic-skill) -->\n---\nname: write-code\ndescription: Rust code style and conventions for Syncpack. Use when writing or modifying Rust code. Covers functional patterns, imports, naming, and quality standards.\n---\n\n# Write Code\n\nRust code conventions for Syncpack.\n\n## Style\n\n- **Functional style:** pipelines over loops\n- **Avoid `?` chains:** use `.and_then()`, `.map()`, `.or_else()`\n- **Descriptive names:** clarity over brevity\n- **Named placeholders:** `println!(\"{var}\")` not `println!(\"{}\", var)`\n- **British English:** \"behaviour\" not \"behavior\", \"organised\" not \"organized\"\n\n## Imports\n\nSingle `use` statement with grouped braces:\n\n```rust\nuse {\n  crate::{cli::Cli, config::Config},\n  log::{debug, error},\n  std::{process::exit, sync::Arc},\n};\n```\n\nRules:\n\n- **Never use `super::`** — always `crate::` for internal imports\n- Group: `crate::`, external crates, `std::`\n- Alphabetise within groups\n\n## File Organisation\n\n| Adding...   | Location                                                         |\n| ----------- | ---------------------------------------------------------------- |\n| New command | `src/commands/{name}.rs`                                         |\n| New test    | Sibling `_test.rs` file (e.g., `src/foo.rs` → `src/foo_test.rs`) |\n\n**NEVER** use `#[cfg(test)]` modules inside implementation files.\n\n## Quality\n\n- Functions <50 lines, commands 100-300 lines\n- Zero warnings (except during TDD red phase)\n- Run `just format` before committing\n\n## Comments\n\n### Forbidden\n\n- **`//!` module docs** — collect implementation history and rot\n- **Phase/F-N labels** — `// Phase v4-2`, `// F16:`, banner headers like `// ---------- Phase v4-3 RED ----------`\n- **History refs** — `Migrated from`, `carry-over`, `Mirrors v3`, `replaces the v3`, `previously inside X`, `reserved for future`\n- **Issue/PR refs** — `Reproduces issue #239`, `GitHub issue #206`. Plans rot; the code is the truth\n- **Banner separators** — `// ---------- Free functions: yaml ops ----------`. Use module structure or let the file speak for itself\n- **Test scenario labels** — don't write `// Windows-style backslashes` above a test input. Use a descriptive variable binding (`let windows_backslashes = [...]`) instead\n- **Name-restating field docs** — `/// A unique identifier for this instance`, `/// The dependency name`, `/// The instance id`. The field name already says it\n- **Step numbering inside functions** — `// 1. ...`, `// 2. ...`. Drop the numbers; keep the WHY if it's non-obvious\n\n### Allowed\n\n- **`@TODO` markers** — future plans worth tracking inline\n- **`///` doc comments** for non-obvious detail: `None`/`Err` semantics, side effects, invariants, distinctions between similar fields (e.g. `is_local_dependency` vs `is_local_instance`)\n- **`//` inline comments** when WHY is non-obvious: hidden constraints, ordering invariants (\"SnappedTo groups must be visited last\"), workarounds for surprising library behaviour, \"must not poison X\" cautions\n\n### Default\n\nNo comments. Add one only when the WHY is non-obvious to a fresh reader.\n\n## Patterns\n\n### Iterating Instances\n\n```rust\nctx.version_groups.iter().for_each(|group| {\n    group.get_sorted_dependencies(&ctx.config.cli.sort).for_each(|dependency| {\n        dependency.get_sorted_instances()\n            .filter(|instance| instance.is_invalid())\n            .for_each(|instance| { /* process */ });\n    });\n});\n```\n\n### Error Handling\n\nPrefer combinators over `?`:\n\n```rust\n// Good\npath.parent()\n    .and_then(|p| p.to_str())\n    .map(|s| s.to_string())\n    .unwrap_or_default()\n\n// Avoid\nlet parent = path.parent()?;\nlet str = parent.to_str()?;\nOk(str.to_string())\n```\n\n### State Mutation\n\n```rust\nlet mut state = instance.state.borrow_mut();\nif !state.is_invalid() {\n    *state = InstanceState::fixable(SomeVariant);\n}\n```\n\n<!-- File: .claude/skills/write-tests/patterns.md (Tokens: ~3249 | Category: anthropic-skill) -->\n# Test patterns\n\nReal, trimmed examples for the common scenarios. Imports and `mod` wrapping omitted for brevity — see [SKILL.md](SKILL.md) for the full preamble.\n\n## Banned\n\n`src/version_group/banned_test.rs`. Removes a dep; refuses to ban a local-package version.\n\n```rust\n#[tokio::test]\nasync fn refuses_to_ban_local_version() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"version\": \"1.0.0\"}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"package-a\": \"1.1.0\"}}),\n    ])\n    .with_version_group(json!({\n      \"dependencies\": [\"package-a\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::suspect(InvalidLocalVersion),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::suspect(RefuseToBanLocal),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"package-a\",\n      id: \"package-a in /dependencies of package-b\",\n      actual: \"1.1.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Banned with `customTypes`\n\n```rust\n#[tokio::test]\nasync fn removes_instance_with_named_version_string_strategy() {\n  let ctx = TestBuilder::new()\n    .with_config(json!({\n      \"customTypes\": {\n        \"packageManager\": {\n          \"strategy\": \"name@version\",\n          \"path\": \"packageManager\"\n        }\n      }\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"packageManager\": \"pnpm@7.27.0\"\n    })])\n    .with_version_group(json!({\n      \"dependencies\": [\"pnpm\"],\n      \"dependencyTypes\": [\"packageManager\"],\n      \"isBanned\": true\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(IsBanned),\n      dependency_name: \"pnpm\",\n      id: \"pnpm in /packageManager of package-a\",\n      actual: \"7.27.0\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Pinned\n\n`src/version_group/pinned_test.rs`.\n\n```rust\n#[tokio::test]\nasync fn an_already_pinned_version_is_valid() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"1.2.0\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIdenticalToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"1.2.0\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n### Pin overrides a semver-group range\n\n```rust\n#[tokio::test]\nasync fn pin_overrides_semver_group_match() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"^1.0.0\"}\n    }))\n    .with_semver_group(json!({\"range\": \"^\", \"dependencies\": [\"foo\"]}))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.0.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::fixable(PinOverridesSemverRange),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"^1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: Some(\"^1.0.0\"),\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Same range\n\n`src/version_group/same_range_test.rs`. Use a `local`-ignored group as guard so the local instances don't pollute the assertion.\n\n```rust\n#[tokio::test]\nasync fn ranges_satisfy_each_other() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"<=2.0.0\"}}),\n    ])\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsIgnored),\n      dependency_name: \"package-b\",\n      id: \"package-b in /version of package-b\",\n      actual: \"\",\n      expected: Some(\"\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-a\",\n      actual: \">=1.0.0\",\n      expected: Some(\">=1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(SatisfiesSameRangeGroup),\n      dependency_name: \"foo\",\n      id: \"foo in /dependencies of package-b\",\n      actual: \"<=2.0.0\",\n      expected: Some(\"<=2.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nFor mismatching ranges the consumer instances become `InstanceState::unfixable(SameRangeMismatch)`.\n\n## Pnpm catalogs\n\n`src/version_group/catalog_test.rs`. `with_pnpm_catalogs` injects `pnpm-workspace.yaml`. Catalog definitions are themselves instances under `/catalog of pnpm-workspace.yaml`.\n\n```rust\n#[tokio::test]\nasync fn catalog_group_definition_valid() {\n  let yaml = \"catalog:\\n  react: ^18.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_version_group(json!({\n      \"label\": \"enforce catalog\",\n      \"dependencies\": [\"react\"],\n      \"policy\": \"catalog\",\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalogDefinition),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^18.0.0\",\n      expected: Some(\"^18.0.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nNamed catalog: yaml `catalogs:\\n  react18:\\n    react: ^18.0.0\\n`, consumer `\"react\": \"catalog:react18\"`, def id `react in /catalogs/react18 of pnpm-workspace.yaml`.\n\n## Bun catalogs\n\n`src/version_group/bun_catalog_test.rs`. Two shapes:\n\n- `with_bun_catalogs(json!({...}))` puts `/catalog`, `/catalogs/{n}` at the synthetic Bun root.\n- `with_bun_workspaces_catalogs(json!({...}))` nests them under `/workspaces/`.\n\nThese tests often use sync `.build()` — they're checking discovery wiring, not the visit pipeline:\n\n```rust\n#[test]\nfn bun_top_level_catalog_definition_emits_instance() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\"name\": \"pkg-a\", \"version\": \"0.0.0\"})])\n    .build();\n  let react = ctx\n    .instances\n    .iter()\n    .find(|i| i.descriptor.name == \"react\")\n    .expect(\"expected a react bun catalog instance\");\n  assert!(react.is_catalog_instance());\n  assert_eq!(react.descriptor.specifier.get_raw(), \"^18.0.0\");\n}\n```\n\nFull validation pass (consumer + def states):\n\n```rust\n#[test]\nfn bun_catalog_definition_with_consumer_valid() {\n  let ctx = TestBuilder::new()\n    .with_bun_catalogs(json!({\n      \"name\": \"bun-root\",\n      \"catalog\": {\"react\": \"^18.0.0\"}\n    }))\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"}\n    })])\n    .build_and_visit_packages();\n  // assert states via expect(&ctx).to_have_instances(...) as elsewhere\n}\n```\n\n## Semver groups\n\nA semver group rewrites the *expected* range; mismatches are `SemverRangeMismatch` (fixable). Combine with version groups to test interaction:\n\n```rust\n#[tokio::test]\nasync fn semver_group_targets_one_package_only() {\n  let ctx = TestBuilder::new()\n    .with_packages(vec![\n      json!({\"name\": \"package-a\", \"dependencies\": {\"foo\": \">=1.0.0\"}}),\n      json!({\"name\": \"package-b\", \"dependencies\": {\"foo\": \"^1.2.3\"}}),\n    ])\n    .with_semver_group(json!({\"packages\": [\"package-b\"], \"range\": \"^\"}))\n    .with_version_groups(vec![\n      json!({\"dependencyTypes\": [\"local\"], \"isIgnored\": true}),\n      json!({\"dependencies\": [\"foo\"], \"policy\": \"sameRange\"}),\n    ])\n    .run()\n    .await;\n  // package-b's `^1.2.3` matches the `^` semver group → no mismatch\n  // assert SatisfiesSameRangeGroup on both consumer instances\n}\n```\n\n## Registry updates\n\n`with_registry_updates` mocks the npm registry and implies subcommand `update`. Optional `with_update_target` bounds it.\n\n```rust\n#[tokio::test]\nasync fn pnpm_catalog_def_marked_outdated_when_registry_has_newer_version() {\n  let yaml = \"catalog:\\n  react: ^17.0.0\\n\";\n  let ctx = TestBuilder::new()\n    .with_pnpm_catalogs(yaml)\n    .with_packages(vec![json!({\n      \"name\": \"pkg-a\",\n      \"version\": \"0.0.0\",\n      \"dependencies\": {\"react\": \"catalog:\"},\n    })])\n    .with_registry_updates(json!({\"react\": [\"17.0.0\", \"18.5.0\"]}))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"pkg-a\",\n      id: \"pkg-a in /version of pkg-a\",\n      actual: \"0.0.0\",\n      expected: Some(\"0.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::valid(IsCatalog),\n      dependency_name: \"react\",\n      id: \"react in /dependencies of pkg-a\",\n      actual: \"catalog:\",\n      expected: Some(\"catalog:\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToNpmRegistry),\n      dependency_name: \"react\",\n      id: \"react in /catalog of pnpm-workspace.yaml\",\n      actual: \"^17.0.0\",\n      expected: Some(\"^18.5.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\nBound the update target:\n\n```rust\nlet ctx = TestBuilder::new()\n  .with_package(json!({\"name\": \"package-a\", \"dependencies\": {\"wat\": \"1.2.3\"}}))\n  .with_update_target(UpdateTarget::Minor)\n  .with_registry_updates(json!({\"wat\": [\"1.2.3\", \"1.3.4\", \"2.0.0\"]}))\n  .run()\n  .await;\n// wat → 1.3.4 (minor cap), not 2.0.0\n```\n\n`UpdateTarget` import: `use crate::cli::UpdateTarget;`.\n\n<!-- File: .claude/skills/write-tests/SKILL.md (Tokens: ~1880 | Category: anthropic-skill) -->\n---\nname: write-tests\ndescription: Write tests for Syncpack using the TestBuilder pattern. Use when adding tests for commands, validation logic, or any new functionality. Covers TestBuilder API, assertion patterns, and common test scenarios.\n---\n\n# Write Tests\n\n## Golden rules\n\n- Use `TestBuilder` — never construct `Context` manually.\n- Assert with `expect(&ctx).to_have_instances(vec![...])` — never index `ctx.instances`.\n- Async-first: `#[tokio::test]` + `.run().await` is the canonical entry.\n\n## TDD\n\n1. Read 2-3 tests in the same `*_test.rs` and copy the pattern\n2. Write failing test → `just test` → confirm RED\n3. Ask before implementing\n4. Implement minimal code → GREEN → `just format`\n\n## Quick start\n\n```rust\nuse {\n  crate::{\n    instance::{FixableInstance::*, InstanceState, SuspectInstance::*, UnfixableInstance::*, ValidInstance::*},\n    test::{\n      builder::TestBuilder,\n      expect::{expect, ExpectedInstance},\n    },\n  },\n  serde_json::json,\n};\n\n#[tokio::test]\nasync fn pinned_version_replaces_anything_different() {\n  let ctx = TestBuilder::new()\n    .with_package(json!({\n      \"name\": \"package-a\",\n      \"version\": \"1.0.0\",\n      \"devDependencies\": {\"foo\": \"workspace:*\"}\n    }))\n    .with_version_group(json!({\n      \"dependencies\": [\"foo\"],\n      \"pinVersion\": \"1.2.0\"\n    }))\n    .run()\n    .await;\n  expect(&ctx).to_have_instances(vec![\n    ExpectedInstance {\n      state: InstanceState::valid(IsLocalAndValid),\n      dependency_name: \"package-a\",\n      id: \"package-a in /version of package-a\",\n      actual: \"1.0.0\",\n      expected: Some(\"1.0.0\"),\n      overridden: None,\n    },\n    ExpectedInstance {\n      state: InstanceState::fixable(DiffersToPin),\n      dependency_name: \"foo\",\n      id: \"foo in /devDependencies of package-a\",\n      actual: \"workspace:*\",\n      expected: Some(\"1.2.0\"),\n      overridden: None,\n    },\n  ]);\n}\n```\n\n## Sub-module organisation\n\nGroup related scenarios under nested modules — see `pinned_test.rs`, `catalog_defs_test.rs`:\n\n```rust\nmod local {\n  use super::*;\n  #[tokio::test] async fn refuses_to_pin_local_version() { ... }\n}\n\nmod normal {\n  use super::*;\n  #[tokio::test] async fn an_already_pinned_version_is_valid() { ... }\n}\n\nmod registry_updates {\n  use super::*;\n  #[tokio::test] async fn def_marked_outdated_when_registry_has_newer_version() { ... }\n}\n```\n\n## File location\n\n| Test type                                    | Location                                    |\n| -------------------------------------------- | ------------------------------------------- |\n| Version-group behaviour (pin, ban, ranges …) | `src/version_group/<group>_test.rs`         |\n| Catalog discovery wiring                     | `src/version_group/{catalog,bun_catalog}_test.rs` |\n| Fix mutations                                | `src/commands/fix_test.rs`                  |\n| Format pass                                  | `src/visit_formatting/format_test.rs`       |\n| Other unit tests                             | Co-located: `src/foo.rs` ↔ `src/foo_test.rs` |\n\n## Builder methods\n\nSource of truth: `src/test/builder.rs`.\n\n| Method                                                  | Purpose                                                              |\n| ------------------------------------------------------- | -------------------------------------------------------------------- |\n| `.with_package(json!({...}))`                           | Add one package.json                                                 |\n| `.with_packages(vec![...])`                             | Add many                                                             |\n| `.with_version_group(json!({...}))`                     | Add one version group                                                |\n| `.with_version_groups(vec![...])`                       | Add many                                                             |\n| `.with_semver_group(json!({...}))`                      | Add a semver group                                                   |\n| `.with_config(json!({...}))`                            | Base config (e.g. `customTypes`, `dependencyGroups`)                 |\n| `.with_strict(bool)`                                    | Strict mode (Suspect → error)                                        |\n| `.with_subcommand(\"update\")`                            | Override subcommand (default: `lint`, or `update` if registry set)   |\n| `.with_pnpm_catalogs(yaml)`                             | Inject `pnpm-workspace.yaml`; implies pnpm PM                        |\n| `.with_bun_catalogs(json!({...}))`                      | Synthetic Bun root with `/catalog`, `/catalogs/{n}`; implies Bun PM  |\n| `.with_bun_workspaces_catalogs(json!({...}))`           | Same, nested under `/workspaces/`                                    |\n| `.with_{pnpm,bun,npm,yarn,unknown}_package_manager()`   | Force PM detection                                                   |\n| `.with_registry_updates(json!({\"react\":[...]}))`        | Mock npm registry; implies subcommand=`update`                       |\n| `.with_update_target(UpdateTarget::Minor)`              | Bound update target                                                  |\n| `.run().await` → `Context`                              | **Primary** end-to-end (full pipeline through disk + discovery)      |\n| `.build()`                                              | Sync, no visit — context-wiring tests                                |\n| `.build_and_visit_packages()`                           | Sync + visit_packages — fix tests, older suites                      |\n| `.build_and_visit_formatting()`                         | Sync + visit_formatting                                              |\n| `.build_with_registry_and_visit().await`                | Sync wiring + async registry mock + visit                            |\n\n## ExpectedInstance fields\n\n```rust\nExpectedInstance {\n  state: InstanceState::fixable(DiffersToPin),  // valid / fixable / unfixable / suspect\n  dependency_name: \"react\",                     // = `internal_name` (alias-aware)\n  id: \"react in /dependencies of package-a\",    // {dep} in {/path} of {package_or_yaml}\n  actual: \"17.0.0\",                             // raw specifier on disk\n  expected: Some(\"18.0.0\"),                     // None = ignore; Some(\"\") = remove\n  overridden: None,                             // semver-group override target, if any\n}\n```\n\n`id` location examples:\n\n- `/dependencies`, `/devDependencies`, `/peerDependencies`\n- `/version of package-a` (local version)\n- `/packageManager`, `/engines/node`\n- `/catalog of pnpm-workspace.yaml`, `/catalogs/<name> of pnpm-workspace.yaml`\n- `/customVersion`, `/custom/config/version` (via `customTypes`)\n\n## Patterns\n\n→ [patterns.md](patterns.md): banned, pinned, sameRange, pnpm catalogs, bun catalogs, semver ranges, registry updates.\n\n## Fix tests\n\n`src/commands/fix_test.rs` builds with `.build_and_visit_packages()` then runs `fix::run(ctx, &SilentReporter, &disk)`. `dry_run = true` is the default (set by `mock::config_from_mock`), so `is_dirty()` and post-fix contents stay observable. Set `ctx.config.cli.dry_run = false` only when asserting writes through a recording `MockDiskIo` (see `pnpm_fix_writes_yaml_to_disk`).\n\n## Common mistakes\n\n| Wrong                                            | Right                                                                                  |\n| ------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| `#[test] fn foo()` + `.run().await`              | `#[tokio::test] async fn foo()`                                                        |\n| `use crate::instance_state::*`                   | `use crate::instance::*`                                                               |\n| `\"pinned\": \"1.0.0\"`                              | `\"pinVersion\": \"1.0.0\"`                                                                |\n| `Context { ... }`                                | `TestBuilder::new()...`                                                                |\n| `ctx.instances[0]`                               | `expect(&ctx).to_have_instances(vec![...])`                                            |\n| `.build()` then check states                     | `.run().await` (or `.build_and_visit_packages()` for sync)                             |\n| Missing `SuspectInstance::*` import              | Import all 4: `FixableInstance::*, ValidInstance::*, SuspectInstance::*, UnfixableInstance::*` |\n\n## Running\n\n```bash\njust test                            # all\ncargo test pinned_test               # pattern match\ncargo test test_name -- --nocapture  # with stdout\n```\n\n## Reference tests\n\n- `src/version_group/banned_test.rs` — banned + custom types\n- `src/version_group/pinned_test.rs` — sub-modules, semver-group interaction\n- `src/version_group/same_range_test.rs` — range satisfaction\n- `src/version_group/catalog_test.rs` — pnpm catalogs\n- `src/version_group/bun_catalog_test.rs` — bun catalogs (sync `.build()`)\n- `src/version_group/preferred_semver_test.rs` — registry updates, update targets\n</yakaai_skills>"}