From 446bd46b95d515bfe426d88b10db4200c93240ca Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 29 Oct 2025 18:21:01 +0800 Subject: [PATCH] feat: scaffold remove command foundations --- AGENTS.md | 2 + internal/remove/errors.go | 8 + internal/remove/parser.go | 34 ++++ internal/remove/request.go | 53 ++++++ internal/remove/summary.go | 113 ++++++++++++ internal/remove/traversal.go | 73 ++++++++ scripts/smoke-test-remove.sh | 57 ++++++ .../checklists/requirements.md | 34 ++++ .../contracts/remove-command.md | 68 +++++++ specs/003-add-remove-command/data-model.md | 47 +++++ specs/003-add-remove-command/plan.md | 94 ++++++++++ specs/003-add-remove-command/quickstart.md | 57 ++++++ specs/003-add-remove-command/research.md | 37 ++++ specs/003-add-remove-command/spec.md | 140 +++++++++++++++ specs/003-add-remove-command/tasks.md | 168 ++++++++++++++++++ tests/fixtures/remove-samples/README.md | 9 + .../remove-samples/basic/archive copy.txt | 0 .../remove-samples/basic/notes draft.txt | 0 .../basic/report copy draft.txt | 0 .../remove-samples/conflicts/alpha foo.txt | 0 .../remove-samples/conflicts/alpha.txt | 0 .../conflicts/project foo draft.txt | 0 .../remove-samples/empties/--placeholder-- | 0 tests/fixtures/remove-samples/empties/draft | 0 .../fixtures/remove-samples/empties/draft.txt | 0 25 files changed, 994 insertions(+) create mode 100644 internal/remove/errors.go create mode 100644 internal/remove/parser.go create mode 100644 internal/remove/request.go create mode 100644 internal/remove/summary.go create mode 100644 internal/remove/traversal.go create mode 100755 scripts/smoke-test-remove.sh create mode 100644 specs/003-add-remove-command/checklists/requirements.md create mode 100644 specs/003-add-remove-command/contracts/remove-command.md create mode 100644 specs/003-add-remove-command/data-model.md create mode 100644 specs/003-add-remove-command/plan.md create mode 100644 specs/003-add-remove-command/quickstart.md create mode 100644 specs/003-add-remove-command/research.md create mode 100644 specs/003-add-remove-command/spec.md create mode 100644 specs/003-add-remove-command/tasks.md create mode 100644 tests/fixtures/remove-samples/README.md create mode 100644 tests/fixtures/remove-samples/basic/archive copy.txt create mode 100644 tests/fixtures/remove-samples/basic/notes draft.txt create mode 100644 tests/fixtures/remove-samples/basic/report copy draft.txt create mode 100644 tests/fixtures/remove-samples/conflicts/alpha foo.txt create mode 100644 tests/fixtures/remove-samples/conflicts/alpha.txt create mode 100644 tests/fixtures/remove-samples/conflicts/project foo draft.txt create mode 100644 tests/fixtures/remove-samples/empties/--placeholder-- create mode 100644 tests/fixtures/remove-samples/empties/draft create mode 100644 tests/fixtures/remove-samples/empties/draft.txt diff --git a/AGENTS.md b/AGENTS.md index e3849e9..2cb9381 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ Auto-generated from all feature plans. Last updated: 2025-10-29 ## Active Technologies - Local filesystem (no persistent database) (002-add-replace-command) - Go 1.24 + `spf13/cobra`, `spf13/pflag` (001-list-command-filters) +- Local filesystem only (ledger persisted as `.renamer`) (003-add-remove-command) ## Project Structure @@ -36,6 +37,7 @@ tests/ - Smoke: `scripts/smoke-test-replace.sh` ## Recent Changes +- 003-add-remove-command: Added Go 1.24 + `spf13/cobra`, `spf13/pflag` - 002-add-replace-command: Added `renamer replace` command, ledger metadata, and automation docs. - 001-list-command-filters: Added `renamer list` command with shared scope flags and formatters. diff --git a/internal/remove/errors.go b/internal/remove/errors.go new file mode 100644 index 0000000..938dd36 --- /dev/null +++ b/internal/remove/errors.go @@ -0,0 +1,8 @@ +package remove + +import "errors" + +var ( + // ErrNoTokens indicates that no removal tokens were provided. + ErrNoTokens = errors.New("at least one non-empty token is required") +) diff --git a/internal/remove/parser.go b/internal/remove/parser.go new file mode 100644 index 0000000..f358624 --- /dev/null +++ b/internal/remove/parser.go @@ -0,0 +1,34 @@ +package remove + +import "strings" + +// ParseArgsResult captures parser output for sequential removals. +type ParseArgsResult struct { + Tokens []string + Duplicates []string +} + +// ParseArgs splits, trims, and deduplicates tokens while preserving order. +func ParseArgs(args []string) (ParseArgsResult, error) { + result := ParseArgsResult{Tokens: make([]string, 0, len(args))} + + seen := make(map[string]int) + for _, raw := range args { + token := strings.TrimSpace(raw) + if token == "" { + continue + } + if _, exists := seen[token]; exists { + result.Duplicates = append(result.Duplicates, token) + continue + } + seen[token] = len(result.Tokens) + result.Tokens = append(result.Tokens, token) + } + + if len(result.Tokens) == 0 { + return ParseArgsResult{}, ErrNoTokens + } + + return result, nil +} diff --git a/internal/remove/request.go b/internal/remove/request.go new file mode 100644 index 0000000..6dedd63 --- /dev/null +++ b/internal/remove/request.go @@ -0,0 +1,53 @@ +package remove + +import ( + "fmt" + + "github.com/rogeecn/renamer/internal/listing" +) + +// Request encapsulates the options required for remove operations. +// It mirrors the listing scope so preview/apply flows stay consistent. +type Request struct { + WorkingDir string + Tokens []string + IncludeDirectories bool + Recursive bool + IncludeHidden bool + Extensions []string +} + +// FromListing builds a Request from the shared listing scope plus ordered tokens. +func FromListing(scope *listing.ListingRequest, tokens []string) (*Request, error) { + if scope == nil { + return nil, fmt.Errorf("scope must not be nil") + } + req := &Request{ + WorkingDir: scope.WorkingDir, + IncludeDirectories: scope.IncludeDirectories, + Recursive: scope.Recursive, + IncludeHidden: scope.IncludeHidden, + Extensions: append([]string(nil), scope.Extensions...), + Tokens: append([]string(nil), tokens...), + } + if err := req.Validate(); err != nil { + return nil, err + } + return req, nil +} + +// Validate ensures the request has the required data before traversal happens. +func (r *Request) Validate() error { + if r.WorkingDir == "" { + return fmt.Errorf("working directory must be provided") + } + if len(r.Tokens) == 0 { + return fmt.Errorf("at least one removal token is required") + } + for i, token := range r.Tokens { + if token == "" { + return fmt.Errorf("token at position %d is empty after trimming", i) + } + } + return nil +} diff --git a/internal/remove/summary.go b/internal/remove/summary.go new file mode 100644 index 0000000..1122d80 --- /dev/null +++ b/internal/remove/summary.go @@ -0,0 +1,113 @@ +package remove + +import "sort" + +// Summary aggregates results across preview/apply phases. +type Summary struct { + totalCandidates int + changedCount int + conflicts []Conflict + empties []string + tokenMatches map[string]int + duplicates []string +} + +// Conflict describes a rename conflict detected during planning. +type Conflict struct { + Original string + Proposed string + Reason string +} + +// NewSummary constructs a ready-to-use Summary. +func NewSummary() Summary { + return Summary{ + tokenMatches: make(map[string]int), + conflicts: make([]Conflict, 0), + empties: make([]string, 0), + duplicates: make([]string, 0), + } +} + +// RecordCandidate increments the total candidate count. +func (s *Summary) RecordCandidate() { + s.totalCandidates++ +} + +// RecordChange increments changed items. +func (s *Summary) RecordChange() { + s.changedCount++ +} + +// AddTokenMatch records the number of matches for a token. +func (s *Summary) AddTokenMatch(token string, count int) { + s.tokenMatches[token] += count +} + +// AddConflict registers a detected conflict. +func (s *Summary) AddConflict(c Conflict) { + s.conflicts = append(s.conflicts, c) +} + +// AddEmpty registers a path skipped due to empty result names. +func (s *Summary) AddEmpty(path string) { + s.empties = append(s.empties, path) +} + +// AddDuplicate tracks duplicate tokens encountered during parsing. +func (s *Summary) AddDuplicate(token string) { + s.duplicates = append(s.duplicates, token) +} + +// TotalCandidates returns how many items were considered. +func (s Summary) TotalCandidates() int { + return s.totalCandidates +} + +// ChangedCount returns the number of items whose names changed. +func (s Summary) ChangedCount() int { + return s.changedCount +} + +// Conflicts returns a copy of conflict info. +func (s Summary) Conflicts() []Conflict { + out := make([]Conflict, len(s.conflicts)) + copy(out, s.conflicts) + return out +} + +// Empties returns paths skipped for empty basename results. +func (s Summary) Empties() []string { + out := make([]string, len(s.empties)) + copy(out, s.empties) + return out +} + +// TokenMatches returns a sorted slice of tokens and counts. +func (s Summary) TokenMatches() []struct { + Token string + Count int +} { + pairs := make([]struct { + Token string + Count int + }, 0, len(s.tokenMatches)) + for token, count := range s.tokenMatches { + pairs = append(pairs, struct { + Token string + Count int + }{Token: token, Count: count}) + } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Token < pairs[j].Token + }) + return pairs +} + +// Duplicates returns duplicates flagged by the parser. +func (s Summary) Duplicates() []string { + out := make([]string, len(s.duplicates)) + copy(out, s.duplicates) + sort.Strings(out) + return out +} diff --git a/internal/remove/traversal.go b/internal/remove/traversal.go new file mode 100644 index 0000000..c4dbc7d --- /dev/null +++ b/internal/remove/traversal.go @@ -0,0 +1,73 @@ +package remove + +import ( + "context" + "io/fs" + "path/filepath" + "strings" + + "github.com/rogeecn/renamer/internal/traversal" +) + +// Candidate represents a file or directory eligible for token removal. +type Candidate struct { + RelativePath string + OriginalPath string + BaseName string + IsDir bool + Depth int +} + +// Traverse walks the working directory according to the request scope and invokes fn for each +// candidate (files by default, directories when IncludeDirectories is true). +func Traverse(ctx context.Context, req *Request, fn func(Candidate) error) error { + if err := req.Validate(); err != nil { + return err + } + + extensions := make(map[string]struct{}, len(req.Extensions)) + for _, ext := range req.Extensions { + lower := strings.ToLower(ext) + extensions[lower] = struct{}{} + } + + walker := traversal.NewWalker() + + return walker.Walk( + req.WorkingDir, + req.Recursive, + req.IncludeDirectories, + req.IncludeHidden, + 0, + func(relPath string, entry fs.DirEntry, depth int) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + isDir := entry.IsDir() + ext := strings.ToLower(filepath.Ext(entry.Name())) + + if !isDir && len(extensions) > 0 { + if _, ok := extensions[ext]; !ok { + return nil + } + } + + candidate := Candidate{ + RelativePath: filepath.ToSlash(relPath), + OriginalPath: filepath.Join(req.WorkingDir, relPath), + BaseName: entry.Name(), + IsDir: isDir, + Depth: depth, + } + + if candidate.RelativePath == "." { + return nil + } + + return fn(candidate) + }, + ) +} diff --git a/scripts/smoke-test-remove.sh b/scripts/smoke-test-remove.sh new file mode 100755 index 0000000..a15d7a2 --- /dev/null +++ b/scripts/smoke-test-remove.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BIN="go run" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +mkdir -p "$TMP_DIR/nested" +touch "$TMP_DIR/report copy draft.txt" +touch "$TMP_DIR/nested/notes draft.txt" + +echo "Previewing removals..." +$BIN "$ROOT_DIR/main.go" remove " copy" " draft" --path "$TMP_DIR" --recursive --dry-run >/dev/null + +if [[ ! -f "$TMP_DIR/report copy draft.txt" ]]; then + echo "preview should not modify files" >&2 + exit 1 +fi + +echo "Applying removals..." +$BIN "$ROOT_DIR/main.go" remove " copy" " draft" --path "$TMP_DIR" --recursive --yes >/dev/null + +if [[ ! -f "$TMP_DIR/report.txt" ]]; then + echo "expected report.txt to exist after removal" >&2 + exit 1 +fi + +if [[ ! -f "$TMP_DIR/nested/notes.txt" ]]; then + echo "expected nested/notes.txt to exist after removal" >&2 + exit 1 +fi + +if [[ -f "$TMP_DIR/report copy draft.txt" ]]; then + echo "source file still exists after apply" >&2 + exit 1 +fi + +echo "Undoing removals..." +$BIN "$ROOT_DIR/main.go" undo --path "$TMP_DIR" >/dev/null + +if [[ ! -f "$TMP_DIR/report copy draft.txt" ]]; then + echo "undo failed to restore report copy draft.txt" >&2 + exit 1 +fi + +if [[ ! -f "$TMP_DIR/nested/notes draft.txt" ]]; then + echo "undo failed to restore nested/notes draft.txt" >&2 + exit 1 +fi + +if [[ -f "$TMP_DIR/report.txt" ]]; then + echo "undo failed to clean up report.txt" >&2 + exit 1 +fi + +echo "Remove smoke test succeeded." diff --git a/specs/003-add-remove-command/checklists/requirements.md b/specs/003-add-remove-command/checklists/requirements.md new file mode 100644 index 0000000..1353ea4 --- /dev/null +++ b/specs/003-add-remove-command/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Remove Command with Sequential Multi-Pattern Support + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-10-29 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` diff --git a/specs/003-add-remove-command/contracts/remove-command.md b/specs/003-add-remove-command/contracts/remove-command.md new file mode 100644 index 0000000..ef0eaf3 --- /dev/null +++ b/specs/003-add-remove-command/contracts/remove-command.md @@ -0,0 +1,68 @@ +# CLI Contract: `renamer remove` + +## Command Synopsis + +```bash +renamer remove [pattern2 ...] [flags] +``` + +### Global Flags (inherited from root command) +- `--path ` (defaults to current working directory) +- `-r`, `--recursive` +- `-d`, `--include-dirs` +- `--hidden` +- `-e`, `--extensions <.ext|.ext2>` +- `--dry-run` +- `--yes` + +## Description +Sequentially removes literal substrings from file and directory names. Every token is applied in the +order provided, and the resulting name is used for subsequent removals before any filesystem rename +occurs. + +## Arguments & Flags + +| Argument | Required | Description | +|----------|----------|-------------| +| `` | Yes (≥1) | Literal substrings to remove sequentially. Quotes required when tokens contain spaces. | + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--path` | string | `.` | Working directory for traversal (global flag). | +| `-r`, `--recursive` | bool | `false` | Traverse subdirectories (global flag). | +| `-d`, `--include-dirs` | bool | `false` | Include directory names in removal scope (global flag). | +| `--hidden` | bool | `false` | Include hidden files/directories (global flag). | +| `-e`, `--extensions` | string | (none) | Restrict removals to files matching `|`-delimited extensions (global flag). | +| `--dry-run` | bool | `false` | Preview only; print proposed removals without applying (global flag). | +| `--yes` | bool | `false` | Apply changes without interactive prompt (global flag). | + +## Exit Codes + +| Code | Meaning | Example Trigger | +|------|---------|-----------------| +| `0` | Success | Preview completed or apply executed without conflicts. | +| `2` | Validation error | No patterns provided, empty token after trimming, unreadable directory. | +| `3` | Conflict detected | Target filename already exists after removals. | + +## Preview Output +- Lists each impacted item with columns: `PATH`, `TOKENS REMOVED`, `NEW PATH`. +- Summary line: `Total: (changed: , empties: , conflicts: )`. + +## Apply Behavior +- Re-validates preview results, then performs renames in sequence while tracking undo metadata. +- Writes ledger entry containing ordered token list and per-token match counts. +- Aborts without partial renames if conflicts arise between preview and apply. + +## Validation Rules +- Tokens are deduplicated case-sensitively but order of first occurrence preserved; duplicates logged + as warnings. +- Resulting names that collapse to empty strings are skipped with warnings. +- Conflicts (targets already existing) abort the operation; users must resolve manually. + +## Examples + +```bash +renamer remove " copy" " draft" +renamer remove foo foo- bar --path ./reports --recursive --dry-run +renamer remove "Project X" " X" --extensions .txt|.md --yes +``` diff --git a/specs/003-add-remove-command/data-model.md b/specs/003-add-remove-command/data-model.md new file mode 100644 index 0000000..879e98a --- /dev/null +++ b/specs/003-add-remove-command/data-model.md @@ -0,0 +1,47 @@ +# Data Model: Remove Command with Sequential Multi-Pattern Support + +## Entities + +### RemoveRequest +- **Description**: Captures inputs driving a remove operation. +- **Fields**: + - `workingDir` (string): Absolute path where traversal begins. + - `tokens` ([]string): Ordered list of literal substrings to remove sequentially. + - `includeDirectories` (bool): Whether directory names participate. + - `recursive` (bool): Whether to traverse subdirectories. + - `includeHidden` (bool): Include hidden files/directories when true. + - `extensions` ([]string): Optional extension filters inherited from global scope flags. + - `dryRun` (bool): Preview flag; true during preview, false for apply. +- **Validations**: + - `tokens` MUST contain at least one non-empty string after trimming. + - `workingDir` MUST exist and be readable prior to traversal. + - `tokens` are deduplicated case-sensitively but order of first occurrence preserved. + +### RemoveSummary +- **Description**: Aggregates preview/apply outcomes for reporting and ledger. +- **Fields**: + - `totalCandidates` (int): Count of files/directories evaluated. + - `changedCount` (int): Count of items whose names change after removals. + - `tokenMatches` (map[string]int): Number of occurrences removed per token (ordered in ledger metadata). + - `conflicts` ([]ConflictDetail): Detected rename conflicts preventing apply. + - `empties` ([]string): Relative paths where removal would lead to empty basename (skipped). + +### ConflictDetail +- **Description**: Captures rename conflicts detected during preview. +- **Fields**: + - `originalPath` (string) + - `proposedPath` (string) + - `reason` (string) + +## Relationships +- `RemoveRequest` feeds traversal utilities to produce candidate names. +- `RemoveSummary` aggregates results from sequential removal engine and is persisted to ledger entries. +- `ConflictDetail` entries inform preview output and determine which renames are skipped. + +## State Transitions +1. **Parse**: CLI arguments parsed into `RemoveRequest`; validations ensure tokens and scope are valid. +2. **Preview**: Sequential removal engine produces proposed names, conflicts, and warnings recorded in + `RemoveSummary`. +3. **Apply**: Upon confirmation/`--yes`, renames execute (in dependency order), ledger entry written + with ordered token metadata. +4. **Undo**: Ledger reverse operation uses stored operations to restore original names. diff --git a/specs/003-add-remove-command/plan.md b/specs/003-add-remove-command/plan.md new file mode 100644 index 0000000..0b3c6fe --- /dev/null +++ b/specs/003-add-remove-command/plan.md @@ -0,0 +1,94 @@ +# Implementation Plan: Remove Command with Sequential Multi-Pattern Support + +**Branch**: `003-add-remove-command` | **Date**: 2025-10-29 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/003-add-remove-command/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +Introduce a `renamer remove` subcommand that deletes multiple literal substrings sequentially while +respecting preview → confirm → ledger → undo guarantees. The command must evaluate all removals in +memory before touching the filesystem to avoid excessive IO and ensure conflict detection happens on +final names. + +## Technical Context + +**Language/Version**: Go 1.24 +**Primary Dependencies**: `spf13/cobra`, `spf13/pflag` +**Storage**: Local filesystem only (ledger persisted as `.renamer`) +**Testing**: Go `testing` package with contract, integration, and unit tests +**Target Platform**: Cross-platform CLI (Linux, macOS, Windows) +**Project Type**: Single CLI project +**Performance Goals**: Preview + apply for 100 files completes within 2 minutes +**Constraints**: Deterministic previews, reversible ledger entries, conflict/empty-name detection before apply +**Scale/Scope**: Handles hundreds of files per invocation; token list expected to be small (≤20 literals) + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- Preview flow MUST show deterministic rename mappings and require explicit confirmation (Preview-First Safety). +- Undo strategy MUST describe how the `.renamer` ledger entry is written and reversed (Persistent Undo Ledger). +- Planned remove rules MUST document their inputs, validations, and composing order (Composable Rule Engine). +- Scope handling MUST cover files vs directories (`-d`), recursion (`-r`), and extension filtering via `-e` without escaping the requested path (Scope-Aware Traversal). +- CLI UX plan MUST confirm Cobra usage, flag naming, help text, and automated tests for preview/undo flows (Ergonomic CLI Stewardship). + +**Gate Alignment**: +- Remove command will reuse preview/confirm pipeline; no rename occurs before preview approval. +- Ledger entries will include ordered tokens removed per file to maintain undo guarantees. +- Removal logic will be implemented as composable rule(s) similar to replace, enabling reuse and testing. +- Command will consume shared scope flags (`--path`, `-r`, `-d`, `--hidden`, `-e`, `--dry-run`, `--yes`). +- Cobra wiring + automated tests will cover help text, sequential behavior warnings, and undo parity. + +## Project Structure + +### Documentation (this feature) + +```text +specs/003-add-remove-command/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +└── tasks.md +``` + +### Source Code (repository root) + +```text +cmd/ +├── root.go +├── list.go +├── replace.go +├── remove.go # new CLI command for sequential removal +└── undo.go + +internal/ +├── listing/ +├── replace/ +├── remove/ # new package mirroring replace for request/parser/engine/summary +├── output/ +├── traversal/ +└── history/ + +scripts/ +├── smoke-test-list.sh +├── smoke-test-replace.sh +└── smoke-test-remove.sh # new end-to-end smoke test + +tests/ +├── contract/ +├── integration/ +├── unit/ +└── fixtures/ +``` + +**Structure Decision**: Single CLI repository with new remove-specific logic under `internal/remove/` +and CLI wiring in `cmd/remove.go`. Tests follow the existing contract/integration/unit layout and a +new smoke test script will live under `scripts/`. + +## Complexity Tracking + +No constitution gate violations identified; no additional complexity justification required. diff --git a/specs/003-add-remove-command/quickstart.md b/specs/003-add-remove-command/quickstart.md new file mode 100644 index 0000000..276dd65 --- /dev/null +++ b/specs/003-add-remove-command/quickstart.md @@ -0,0 +1,57 @@ +# Quickstart: Remove Command with Sequential Multi-Pattern Support + +## Goal +Demonstrate how to delete multiple substrings sequentially from filenames while using the +preview → apply → undo workflow safely. + +## Prerequisites +- Go toolchain (>= 1.24) installed for building the CLI locally. +- Sample directory containing files with recurring tokens (e.g., `draft`, `copy`). + +## Steps + +1. **Build the CLI** + ```bash + go build -o renamer ./... + ``` + +2. **Inspect remove help** + ```bash + ./renamer remove --help + ``` + Pay attention to sequential behavior: tokens execute in the order provided. + +3. **Run a preview with multiple tokens** + ```bash + ./renamer remove " copy" " draft" --path ./samples --dry-run + ``` + Confirm the output table shows each token removed in order and the summary reflects changed files. + +4. **Apply removals after review** + ```bash + ./renamer remove " copy" " draft" --path ./samples --yes + ``` + Verify filenames no longer contain the tokens and a ledger entry is created. + +5. **Undo if necessary** + ```bash + ./renamer undo --path ./samples + ``` + Ensure filenames return to their original state. + +6. **Handle empty-result warnings** + ```bash + ./renamer remove "project" "project-" --path ./samples --dry-run + ``` + Expect the preview to warn and skip items that would collapse to empty names. + +7. **Integrate into automation** + ```bash + ./renamer remove foo bar --dry-run && \ + ./renamer remove foo bar --yes --path ./automation + ``` + Use non-zero exit codes to detect invalid input in scripts. + +## Next Steps +- Add contract tests covering sequential removals and empty-name warnings. +- Extend documentation (CLI reference, changelog) with remove command examples. diff --git a/specs/003-add-remove-command/research.md b/specs/003-add-remove-command/research.md new file mode 100644 index 0000000..0450b62 --- /dev/null +++ b/specs/003-add-remove-command/research.md @@ -0,0 +1,37 @@ +# Phase 0 Research: Remove Command with Sequential Multi-Pattern Support + +## Decision: Sequential removal executed in-memory before filesystem writes +- **Rationale**: Computing the full rename plan in memory guarantees deterministic previews, + simplifies conflict detection, and avoids partial renames that could increase IO load or leave the + filesystem inconsistent. +- **Alternatives considered**: + - *Apply-and-check per token*: rejected due to repeated filesystem mutations and difficulty keeping + undo history coherent. + - *Streaming rename per file*: rejected because conflicts can only be detected after all tokens + apply. + +## Decision: Dedicated `internal/remove` package mirroring replace architecture +- **Rationale**: Keeps responsibilities separated (parser, engine, summary) and allows reuse of + traversal/history helpers. Aligns with Composable Rule Engine principle. +- **Alternatives considered**: + - *Extending replace package*: rejected to avoid coupling distinct behaviors and tests. + - *Embedding logic directly in command*: rejected for testability and maintainability reasons. + +## Decision: Empty-result handling warns and skips rename +- **Rationale**: Removing multiple tokens could produce empty basenames; skipping prevents creating + invalid filenames while still informing the user. +- **Alternatives considered**: + - *Allow empty names*: rejected as unsafe and difficult to undo cleanly on certain filesystems. + - *Hard fail entire batch*: rejected because unaffected files should still be processed. + +## Decision: Ledger metadata records ordered tokens and counts +- **Rationale**: Automation and undo workflows need insight into which tokens were removed and how + often, mirroring replace’s metadata for consistency. +- **Alternatives considered**: + - *Only store operations*: insufficient for auditing complex removals. + +## Decision: CLI help & quickstart emphasize ordering semantics +- **Rationale**: Sequential behavior is the primary mental model difference from other commands; clear + documentation reduces support load and user confusion. +- **Alternatives considered**: + - *Rely on examples alone*: risk of users assuming parallel removal and encountering surprises. diff --git a/specs/003-add-remove-command/spec.md b/specs/003-add-remove-command/spec.md new file mode 100644 index 0000000..8130706 --- /dev/null +++ b/specs/003-add-remove-command/spec.md @@ -0,0 +1,140 @@ +# Feature Specification: Remove Command with Sequential Multi-Pattern Support + +**Feature Branch**: `003-add-remove-command` +**Created**: 2025-10-29 +**Status**: Draft +**Input**: User description: "添加 移除(Remove)命令,用于删除指定字符、字符串,支持同时删除多个字符串,示例:renamer remove str1 str2 ....,注意:多个移除时后续参数的移除依赖于前一个移除后的结果,移除计算完成前不进行重命名,避免IO负载过高" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Remove Unwanted Tokens in One Pass (Priority: P1) + +As a CLI user tidying filenames, I want `renamer remove` to delete multiple substrings in order so I +can normalize file names without writing custom scripts. + +**Why this priority**: Delivers the core value—batch cleanup of recurring tokens across many files +with predictable results. + +**Independent Test**: Run `renamer remove " copy" " draft" --dry-run` in a sample directory, +confirm preview shows the ordered removal effects, then apply with `--yes` and verify names update +accordingly. + +**Acceptance Scenarios**: + +1. **Given** files `report copy draft.txt` and `notes draft.txt`, **When** the user runs + `renamer remove " copy" " draft"`, **Then** the preview shows both tokens removed sequentially + and execution produces `report.txt` and `notes.txt`. +2. **Given** patterns where later removals depend on earlier results (e.g., removing `foo` then `foo-`), + **When** the command runs, **Then** each removal applies to the output of the previous step before + computing rename conflicts. + +--- + +### User Story 2 - Script-Friendly Removal Workflow (Priority: P2) + +As an operator automating rename tasks, I want deterministic previews, exit codes, and ledger entries +so scripts can run `renamer remove` safely without interactive prompts. + +**Why this priority**: Ensures automation pipelines can rely on the same safety guarantees as manual +runs. + +**Independent Test**: In a CI script, call `renamer remove ... --dry-run`, assert exit code 0, then +run with `--yes` and verify ledger entry plus `renamer undo` restores originals. + +**Acceptance Scenarios**: + +1. **Given** a non-interactive context, **When** the user passes `--yes` after a successful preview, + **Then** the command exits 0 on success and writes a ledger entry capturing tokens removed per file. +2. **Given** invalid input (e.g., fewer than two arguments), **When** the command executes, **Then** it + exits with non-zero status and instructs the user on correct sequential argument usage. + +--- + +### User Story 3 - Validate Sequential Removal Inputs (Priority: P3) + +As a power user managing complex token lists, I want clear validation and guidance for spaces, +duplicate tokens, and results that could produce empty filenames so I can adjust before applying. + +**Why this priority**: Prevents surprise failures when tokens overlap or yield empty names. + +**Independent Test**: Run `renamer remove "Project X" " Project" "X" --dry-run`, confirm preview +shows the sequential impact and warns if names collapse; invalid quoting should produce actionable +errors. + +**Acceptance Scenarios**: + +1. **Given** duplicate tokens, **When** the command runs, **Then** duplicates are deduplicated with a + warning and order preserved for remaining unique tokens. +2. **Given** a removal sequence that would produce an empty basename, **When** the preview runs, + **Then** the command warns and excludes the rename unless the user overrides in a future version. + +--- + +### Edge Cases + +- Sequential removals should operate on the output of prior removals within the same filename. +- Removing tokens may collapse names to empty strings or leave trailing separators; preview must flag + these cases before apply. +- Resulting names may collide; conflicts must be reported before confirmation. +- Hidden files or directories may need inclusion depending on scope flags (`--hidden`). +- No matches should result in a friendly "No entries matched" message and exit code 0 without ledger + writes. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The CLI MUST expose `renamer remove [pattern2 ...]` where all tokens are + literal substrings removed sequentially; final name computation MUST complete before any renames + occur. +- **FR-002**: Preview → confirm workflow MUST mirror existing commands, listing original and proposed + names with highlighted removals. +- **FR-003**: Executions MUST append detailed entries to `.renamer` including original names, tokens + removed (with order), resulting names, and timestamps so undo remains possible. +- **FR-004**: Users MUST be able to undo the most recent remove batch via existing undo mechanics + without leaving orphaned files. +- **FR-005**: Command MUST respect global scope flags (`--path`, `--recursive`, `--include-dirs`, + `--hidden`, `--extensions`, `--dry-run`, `--yes`) identical to `list` / `replace` behavior. +- **FR-006**: Preview MUST evaluate all removals first, calculate conflicts, and only then apply + filesystem operations when confirmed, limiting IO load. +- **FR-007**: Command MUST warn (and skip) renames that would result in empty basenames unless a + future explicit override flag is provided. +- **FR-008**: Invalid invocations (fewer than two arguments, empty tokens after trimming) MUST fail + with exit code ≠0 and actionable usage guidance. +- **FR-009**: Help output MUST document sequential behavior, whitespace quoting, and interaction with + other scope flags. + +### Key Entities + +- **RemoveRequest**: Captures working directory, scope flags, ordered token list, and preview/apply + options. +- **RemoveSummary**: Aggregates per-token match counts, per-file outcomes, conflicts, and warnings for + preview and ledger output. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Users complete a sequential removal across 100 files (preview + apply) in under 2 + minutes end-to-end. +- **SC-002**: 95% of usability test participants correctly understand that removals execute in the + provided order after reading `renamer remove --help`. +- **SC-003**: Automated regression tests confirm remove + undo leave the filesystem unchanged in + 100% of scripted scenarios. +- **SC-004**: Support requests related to manual substring cleanup drop by 35% within the first release + cycle after launch. + +## Assumptions + +- Removals are literal substring matches; regex or wildcard support is out of scope for this release. +- Default matching is case-sensitive; case-insensitive options can be considered later if needed. +- Delete operations target filenames (and directories when `-d/--include-dirs` is set), not file + contents. +- Existing traversal, conflict detection, and ledger infrastructure can be extended for the remove + command. + +## Dependencies & Risks + +- Requires new remove-specific packages analogous to replace to maintain modularity. +- Help/quickstart documentation must be updated to explain sequential removal behavior. +- Potential filename conflicts or empty results must be detected pre-apply to avoid data loss. diff --git a/specs/003-add-remove-command/tasks.md b/specs/003-add-remove-command/tasks.md new file mode 100644 index 0000000..efa17db --- /dev/null +++ b/specs/003-add-remove-command/tasks.md @@ -0,0 +1,168 @@ +# Tasks: Remove Command with Sequential Multi-Pattern Support + +**Input**: Design documents from `/specs/003-add-remove-command/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Prepare shared fixtures and tooling used across all remove command user stories. + +- [X] T001 Create remove command fixture directories (`basic/`, `conflicts/`, `empties/`) with placeholder files and README in `tests/fixtures/remove-samples/`. +- [X] T002 [P] Author baseline smoke script showing preview → apply → undo flow for remove command in `scripts/smoke-test-remove.sh`. + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core remove command structures required before any user story implementation. + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete. + +- [X] T003 [P] Define `RemoveRequest` struct plus validation and scope adaptation helpers in `internal/remove/request.go`. +- [X] T004 [P] Implement argument parsing (trimming, minimum token count, parse result object) in `internal/remove/parser.go`. +- [X] T005 [P] Create remove summary types and aggregation helpers for counts/conflicts in `internal/remove/summary.go`. +- [X] T006 Build traversal adapter that reuses listing scope to enumerate candidates for removal in `internal/remove/traversal.go`. + +**Checkpoint**: Foundation ready—user story implementation can now begin in parallel. + +--- + +## Phase 3: User Story 1 - Remove Unwanted Tokens in One Pass (Priority: P1) 🎯 MVP + +**Goal**: Deliver sequential substring removal with preview and apply, covering the core filename cleanup workflow. + +**Independent Test**: Run `renamer remove " copy" " draft" --dry-run` against `tests/fixtures/remove-samples/basic`, verify preview ordering, then apply with `--yes` and confirm filesystem changes. + +### Tests for User Story 1 ⚠️ + +- [ ] T007 [P] [US1] Add unit tests covering sequential token application and unchanged cases in `tests/unit/remove_engine_test.go`. +- [ ] T008 [P] [US1] Create contract test validating preview table output and dry-run messaging in `tests/contract/remove_command_preview_test.go`. +- [ ] T009 [P] [US1] Write integration test exercising preview → apply flow with multiple files in `tests/integration/remove_flow_test.go`. + +### Implementation for User Story 1 + +- [ ] T010 [US1] Implement sequential removal engine producing planned operations in `internal/remove/engine.go`. +- [ ] T011 [US1] Build preview pipeline that aggregates summaries, detects conflicts, and streams output in `internal/remove/preview.go`. +- [ ] T012 [US1] Implement apply pipeline executing planned operations without ledger writes in `internal/remove/apply.go`. +- [ ] T013 [US1] Wire new Cobra command in `cmd/remove.go` (with registration in `cmd/root.go`) to drive preview/apply using shared scope flags. + +**Checkpoint**: User Story 1 functional end-to-end with preview/apply validated by automated tests. + +--- + +## Phase 4: User Story 2 - Script-Friendly Removal Workflow (Priority: P2) + +**Goal**: Ensure automation can run `renamer remove` non-interactively with deterministic exit codes and ledger-backed undo. + +**Independent Test**: Execute `renamer remove foo bar --dry-run` followed by `--yes` inside CI fixture, verify exit code 0 on success, ledger metadata persists tokens, and `renamer undo` restores originals. + +### Tests for User Story 2 ⚠️ + +- [ ] T014 [P] [US2] Add contract test asserting ledger entries capture ordered tokens and match counts in `tests/contract/remove_command_ledger_test.go`. +- [ ] T015 [P] [US2] Add integration test covering `--yes` automation path and subsequent undo in `tests/integration/remove_undo_test.go`. + +### Implementation for User Story 2 + +- [ ] T016 [US2] Extend apply pipeline to append ledger entries with ordered tokens and match counts in `internal/remove/apply.go`. +- [ ] T017 [US2] Update `cmd/remove.go` to support non-interactive `--yes` execution, emit automation-oriented messages, and propagate exit codes. + +**Checkpoint**: User Story 2 complete—CLI safe for scripting with ledger + undo parity. + +--- + +## Phase 5: User Story 3 - Validate Sequential Removal Inputs (Priority: P3) + +**Goal**: Provide clear validation and warnings for duplicate tokens, empty results, and risky removals. + +**Independent Test**: Run `renamer remove "Project X" " Project" "X" --dry-run` and confirm duplicate dedupe warnings plus empty-result skips appear in preview output. + +### Tests for User Story 3 ⚠️ + +- [ ] T018 [P] [US3] Add parser validation tests for duplicate tokens and whitespace edge cases in `tests/unit/remove_parser_test.go`. +- [ ] T019 [P] [US3] Add integration test verifying empty-basename warnings and skips in `tests/integration/remove_validation_test.go`. + +### Implementation for User Story 3 + +- [ ] T020 [US3] Implement duplicate token deduplication with ordered warning collection in `internal/remove/parser.go`. +- [ ] T021 [US3] Add empty-basename detection and warning tracking in `internal/remove/summary.go`. +- [ ] T022 [US3] Surface duplicate and empty warnings in CLI output handling within `cmd/remove.go`. + +**Checkpoint**: All user stories deliver value; validations prevent risky rename plans. + +--- + +## Final Phase: Polish & Cross-Cutting Concerns + +**Purpose**: Documentation, tooling, and quality improvements spanning all user stories. + +- [ ] T023 [P] Update remove command documentation and sequential behavior guidance in `docs/cli-flags.md`. +- [ ] T024 Record release notes for remove command launch in `docs/CHANGELOG.md`. +- [ ] T025 [P] Finalize `scripts/smoke-test-remove.sh` with assertions and integrate into CI instructions. +- [ ] T026 Add remove command walkthrough to project onboarding materials in `AGENTS.md`. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies—start immediately. +- **Foundational (Phase 2)**: Depends on Setup completion—BLOCKS all user stories. +- **User Stories (Phase 3–5)**: Each depends on Foundational phase; implement in priority order (P1 → P2 → P3) or in parallel once shared blockers clear. +- **Polish (Final Phase)**: Depends on completion of targeted user stories. + +### User Story Dependencies + +- **US1 (P1)**: Requires Foundational tasks (T003–T006). +- **US2 (P2)**: Requires US1 core command and apply pipeline (T010–T013). +- **US3 (P3)**: Requires parser and summary scaffolding plus US1 preview pipeline (T004–T013). + +### Within Each User Story + +- Tests (if included) MUST be authored before implementation tasks. +- Engine/traversal logic precedes CLI wiring for predictable integration. +- Command wiring completes only after engine/preview/apply logic is ready. + +### Parallel Opportunities + +- Setup tasks (T001–T002) can run in parallel. +- Foundational tasks marked [P] (T003–T005) may proceed concurrently after directory scaffolding. +- US1 test tasks (T007–T009) can run in parallel once fixtures exist. +- US2 and US3 test tasks (T014–T019) can execute concurrently after their respective foundations. +- Polish tasks marked [P] (T023, T025) can occur alongside documentation updates. + +--- + +## Parallel Example: User Story 1 + +```bash +# Parallel test development for US1: +# - T007: tests/unit/remove_engine_test.go +# - T008: tests/contract/remove_command_preview_test.go +# - T009: tests/integration/remove_flow_test.go +# +# Once tests are in place, run them together: +go test ./tests/unit ./tests/contract ./tests/integration -run Remove +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1 (Setup) and Phase 2 (Foundational). +2. Finish Phase 3 (US1) delivering preview/apply with automated coverage. +3. Validate with `go test ./...` and smoke script before moving on. + +### Incremental Delivery + +1. Deliver US1 (core removal) → release MVP. +2. Add US2 (automation + ledger) → publish update. +3. Enhance with US3 (advanced validation) → finalize release notes. + +### Parallel Team Strategy + +- After Phase 2, one developer tackles US1 implementation while another starts US2 tests. +- US3 validation enhancements can begin once parser scaffolding lands, overlapping with documentation polish. +- Conclude with Polish phase tasks to align docs, smoke tests, and onboarding materials. diff --git a/tests/fixtures/remove-samples/README.md b/tests/fixtures/remove-samples/README.md new file mode 100644 index 0000000..1f7cd51 --- /dev/null +++ b/tests/fixtures/remove-samples/README.md @@ -0,0 +1,9 @@ +# Remove Command Fixtures + +Sample directory structures used by remove command tests. Keep filenames ASCII and small. + +- `basic/` — General-purpose samples demonstrating sequential token removals. +- `conflicts/` — Files that collide after token removal to exercise conflict handling. +- `empties/` — Names that collapse to empty or near-empty results to validate warnings. + +Add directories/files sparingly so tests stay fast and portable. diff --git a/tests/fixtures/remove-samples/basic/archive copy.txt b/tests/fixtures/remove-samples/basic/archive copy.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/basic/notes draft.txt b/tests/fixtures/remove-samples/basic/notes draft.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/basic/report copy draft.txt b/tests/fixtures/remove-samples/basic/report copy draft.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/conflicts/alpha foo.txt b/tests/fixtures/remove-samples/conflicts/alpha foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/conflicts/alpha.txt b/tests/fixtures/remove-samples/conflicts/alpha.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/conflicts/project foo draft.txt b/tests/fixtures/remove-samples/conflicts/project foo draft.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/empties/--placeholder-- b/tests/fixtures/remove-samples/empties/--placeholder-- new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/empties/draft b/tests/fixtures/remove-samples/empties/draft new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/remove-samples/empties/draft.txt b/tests/fixtures/remove-samples/empties/draft.txt new file mode 100644 index 0000000..e69de29