Add extension normalization command
This commit is contained in:
34
specs/004-extension-rename/checklists/requirements.md
Normal file
34
specs/004-extension-rename/checklists/requirements.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Specification Quality Checklist: Extension Command for Multi-Extension Normalization
|
||||
|
||||
**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
|
||||
|
||||
- Specification validated on 2025-10-29.
|
||||
248
specs/004-extension-rename/contracts/extension-command.yaml
Normal file
248
specs/004-extension-rename/contracts/extension-command.yaml
Normal file
@@ -0,0 +1,248 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Renamer Extension Command API
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Contract representation of the `renamer extension` command workflows (preview/apply/undo)
|
||||
for testing harnesses and documentation parity.
|
||||
servers:
|
||||
- url: cli://renamer
|
||||
description: Command-line invocation surface
|
||||
paths:
|
||||
/extension/preview:
|
||||
post:
|
||||
summary: Preview extension normalization results
|
||||
description: Mirrors `renamer extension [source-ext...] [target-ext] --dry-run`
|
||||
operationId: previewExtension
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExtensionRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful preview
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExtensionPreview'
|
||||
'400':
|
||||
description: Validation error (invalid extensions, missing arguments)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/extension/apply:
|
||||
post:
|
||||
summary: Apply extension normalization
|
||||
description: Mirrors `renamer extension [source-ext...] [target-ext] --yes`
|
||||
operationId: applyExtension
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ExtensionRequest'
|
||||
- type: object
|
||||
properties:
|
||||
dryRun:
|
||||
type: boolean
|
||||
const: false
|
||||
responses:
|
||||
'200':
|
||||
description: Successful apply (may include no-change entries)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExtensionApplyResult'
|
||||
'409':
|
||||
description: Conflict detected; apply refused
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ConflictResponse'
|
||||
'400':
|
||||
description: Validation error (invalid extensions, missing arguments)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/extension/undo:
|
||||
post:
|
||||
summary: Undo the most recent extension batch
|
||||
description: Mirrors `renamer undo` when last ledger entry was from extension command.
|
||||
operationId: undoExtension
|
||||
responses:
|
||||
'200':
|
||||
description: Undo succeeded and ledger updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UndoResult'
|
||||
'409':
|
||||
description: Ledger inconsistent or missing entry
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
components:
|
||||
schemas:
|
||||
ExtensionRequest:
|
||||
type: object
|
||||
required:
|
||||
- workingDir
|
||||
- sourceExtensions
|
||||
- targetExtension
|
||||
properties:
|
||||
workingDir:
|
||||
type: string
|
||||
description: Absolute path for command scope.
|
||||
sourceExtensions:
|
||||
type: array
|
||||
description: Ordered list of dot-prefixed extensions to normalize (case-insensitive uniqueness).
|
||||
minItems: 1
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\\.[\\w\\-]+$'
|
||||
targetExtension:
|
||||
type: string
|
||||
description: Dot-prefixed extension applied to all matches.
|
||||
pattern: '^\\.[\\w\\-]+$'
|
||||
includeDirs:
|
||||
type: boolean
|
||||
default: false
|
||||
recursive:
|
||||
type: boolean
|
||||
default: false
|
||||
includeHidden:
|
||||
type: boolean
|
||||
default: false
|
||||
extensionFilter:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\\.[\\w\\-]+$'
|
||||
dryRun:
|
||||
type: boolean
|
||||
default: true
|
||||
autoConfirm:
|
||||
type: boolean
|
||||
default: false
|
||||
ExtensionPreview:
|
||||
type: object
|
||||
required:
|
||||
- totalCandidates
|
||||
- totalChanged
|
||||
- noChange
|
||||
- entries
|
||||
properties:
|
||||
totalCandidates:
|
||||
type: integer
|
||||
minimum: 0
|
||||
totalChanged:
|
||||
type: integer
|
||||
minimum: 0
|
||||
noChange:
|
||||
type: integer
|
||||
minimum: 0
|
||||
conflicts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Conflict'
|
||||
warnings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
entries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PreviewEntry'
|
||||
ExtensionApplyResult:
|
||||
type: object
|
||||
required:
|
||||
- totalApplied
|
||||
- noChange
|
||||
- ledgerEntryId
|
||||
properties:
|
||||
totalApplied:
|
||||
type: integer
|
||||
minimum: 0
|
||||
noChange:
|
||||
type: integer
|
||||
minimum: 0
|
||||
ledgerEntryId:
|
||||
type: string
|
||||
warnings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
UndoResult:
|
||||
type: object
|
||||
required:
|
||||
- restored
|
||||
- ledgerEntryId
|
||||
properties:
|
||||
restored:
|
||||
type: integer
|
||||
ledgerEntryId:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
Conflict:
|
||||
type: object
|
||||
required:
|
||||
- originalPath
|
||||
- proposedPath
|
||||
- reason
|
||||
properties:
|
||||
originalPath:
|
||||
type: string
|
||||
proposedPath:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
enum:
|
||||
- duplicate_target
|
||||
- existing_file
|
||||
PreviewEntry:
|
||||
type: object
|
||||
required:
|
||||
- originalPath
|
||||
- proposedPath
|
||||
- status
|
||||
properties:
|
||||
originalPath:
|
||||
type: string
|
||||
proposedPath:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- changed
|
||||
- no_change
|
||||
- skipped
|
||||
sourceExtension:
|
||||
type: string
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
remediation:
|
||||
type: string
|
||||
ConflictResponse:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
- conflicts
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
conflicts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Conflict'
|
||||
66
specs/004-extension-rename/data-model.md
Normal file
66
specs/004-extension-rename/data-model.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Data Model – Extension Command
|
||||
|
||||
## Entity: ExtensionRequest
|
||||
- **Fields**
|
||||
- `WorkingDir string` — Absolute path resolved from CLI `--path` or current directory.
|
||||
- `SourceExtensions []string` — Ordered list of unique, dot-prefixed source extensions (case-insensitive comparisons).
|
||||
- `TargetExtension string` — Dot-prefixed extension applied verbatim during rename.
|
||||
- `IncludeDirs bool` — Mirrors `--include-dirs` scope flag.
|
||||
- `Recursive bool` — Mirrors `--recursive` traversal flag.
|
||||
- `IncludeHidden bool` — True only when `--hidden` supplied.
|
||||
- `ExtensionFilter []string` — Normalized extension filter from `--extensions`, applied before source matching.
|
||||
- `DryRun bool` — Indicates preview-only execution (`--dry-run`).
|
||||
- `AutoConfirm bool` — Captures `--yes` for non-interactive apply.
|
||||
- `Timestamp time.Time` — Invocation timestamp for ledger correlation.
|
||||
- **Validation Rules**
|
||||
- Require at least one source extension and one target extension (total args ≥ 2).
|
||||
- All extensions MUST start with `.` and contain ≥1 alphanumeric character after the dot.
|
||||
- Deduplicate source extensions case-insensitively; warn on duplicates/no-ops.
|
||||
- Target extension MUST NOT be empty and MUST include leading dot.
|
||||
- Reject empty string tokens after trimming whitespace.
|
||||
- **Relationships**
|
||||
- Consumed by the extension rule engine to enumerate candidate files.
|
||||
- Serialized into ledger metadata alongside `ExtensionSummary`.
|
||||
|
||||
## Entity: ExtensionSummary
|
||||
- **Fields**
|
||||
- `TotalCandidates int` — Number of filesystem entries examined post-scope filtering.
|
||||
- `TotalChanged int` — Count of files scheduled for rename (target extension applied).
|
||||
- `NoChange int` — Count of files already matching `TargetExtension`.
|
||||
- `PerExtensionCounts map[string]int` — Matches per source extension (case-insensitive key).
|
||||
- `Conflicts []Conflict` — Entries describing colliding target paths.
|
||||
- `Warnings []string` — Validation and scope warnings (duplicates, no matches, hidden skipped).
|
||||
- `PreviewEntries []PreviewEntry` — Ordered list of original/new paths with status `changed|no_change|skipped`.
|
||||
- `LedgerMetadata map[string]any` — Snapshot persisted with ledger entry (sources, target, flags).
|
||||
- **Validation Rules**
|
||||
- Conflicts list MUST be empty before allowing apply.
|
||||
- `NoChange + TotalChanged` MUST equal count of entries included in preview.
|
||||
- Preview entries MUST be deterministic (stable sort by original path).
|
||||
- **Relationships**
|
||||
- Emitted to preview renderer for CLI output formatting.
|
||||
- Persisted with ledger entry for undo operations and audits.
|
||||
|
||||
## Entity: Conflict
|
||||
- **Fields**
|
||||
- `OriginalPath string` — Existing file path causing the collision.
|
||||
- `ProposedPath string` — Target path that clashes with another candidate.
|
||||
- `Reason string` — Short code (e.g., `duplicate_target`, `existing_file`) describing conflict type.
|
||||
- **Validation Rules**
|
||||
- `ProposedPath` MUST be unique across non-conflicting entries.
|
||||
- Reasons limited to known enum for consistent messaging.
|
||||
- **Relationships**
|
||||
- Referenced within `ExtensionSummary.Conflicts`.
|
||||
- Propagated to CLI preview warnings.
|
||||
|
||||
## Entity: PreviewEntry
|
||||
- **Fields**
|
||||
- `OriginalPath string`
|
||||
- `ProposedPath string`
|
||||
- `Status string` — `changed`, `no_change`, or `skipped`.
|
||||
- `SourceExtension string` — Detected source extension (normalized lowercase).
|
||||
- **Validation Rules**
|
||||
- `Status` MUST be one of the defined constants.
|
||||
- `ProposedPath` MUST equal `OriginalPath` when `Status == "no_change"`.
|
||||
- **Relationships**
|
||||
- Aggregated into `ExtensionSummary.PreviewEntries`.
|
||||
- Used by preview renderer and ledger writer.
|
||||
108
specs/004-extension-rename/plan.md
Normal file
108
specs/004-extension-rename/plan.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Implementation Plan: Extension Command for Multi-Extension Normalization
|
||||
|
||||
**Branch**: `004-extension-rename` | **Date**: 2025-10-30 | **Spec**: `specs/004-extension-rename/spec.md`
|
||||
**Input**: Feature specification from `/specs/004-extension-rename/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver a `renamer extension` subcommand that normalizes one or more source extensions to a single target extension with deterministic previews, ledger-backed undo, and automation-friendly exit codes. The implementation will extend existing replace/remove infrastructure, ensuring case-insensitive extension matching, hidden-file opt-in, conflict detection, and “no change” handling for already-targeted files.
|
||||
|
||||
## Technical Context
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the content in this section with the technical details
|
||||
for the project. The structure here is presented in advisory capacity to guide
|
||||
the iteration process.
|
||||
-->
|
||||
|
||||
**Language/Version**: Go 1.24
|
||||
**Primary Dependencies**: `spf13/cobra`, `spf13/pflag`, internal traversal/ledger packages
|
||||
**Storage**: Local filesystem + `.renamer` ledger files
|
||||
**Testing**: `go test ./...`, smoke + integration scripts under `tests/` and `scripts/`
|
||||
**Target Platform**: Cross-platform CLI (Linux, macOS, Windows shells)
|
||||
**Project Type**: Single CLI project (`cmd/`, `internal/`, `tests/`, `scripts/`)
|
||||
**Performance Goals**: Normalize 500 files (preview+apply) in <2 minutes end-to-end
|
||||
**Constraints**: Preview-first workflow, ledger append-only, hidden files excluded unless `--hidden`
|
||||
**Scale/Scope**: Operates on thousands of filesystem entries per invocation within local directory trees
|
||||
|
||||
## 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). ✅ Extend existing preview engine to list original → target paths, highlighting extension changes and “no change” rows; apply remains gated by `--yes`.
|
||||
- Undo strategy MUST describe how the `.renamer` ledger entry is written and reversed (Persistent Undo Ledger). ✅ Record source extension list, target, per-file outcomes in ledger entry and ensure undo replays entries via existing ledger service.
|
||||
- Planned rename rules MUST document their inputs, validations, and composing order (Composable Rule Engine). ✅ Define an extension normalization rule that consumes scope matches, validates tokens, deduplicates case-insensitively, and reuses sequencing from replace engine.
|
||||
- Scope handling MUST cover files vs directories (`-d`), recursion (`-r`), and extension filtering via `-e` without escaping the requested path (Scope-Aware Traversal). ✅ Continue relying on shared traversal component honoring flags; ensure hidden assets stay excluded unless `--hidden`.
|
||||
- CLI UX plan MUST confirm Cobra usage, flag naming, help text, and automated tests for preview/undo flows (Ergonomic CLI Stewardship). ✅ Implement `extension` Cobra command mirroring existing flag sets, update help text, and add contract/integration tests for preview, apply, and undo.
|
||||
|
||||
*Post-Design Verification (2025-10-30): Research and design artifacts document preview flow, ledger metadata, rule composition, scope behavior, and Cobra UX updates—no gate violations detected.*
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/speckit.plan command output)
|
||||
├── research.md # Phase 0 output (/speckit.plan command)
|
||||
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
|
||||
for this feature. Delete unused options and expand the chosen structure with
|
||||
real paths (e.g., apps/admin, packages/something). The delivered plan must
|
||||
not include Option labels.
|
||||
-->
|
||||
|
||||
```text
|
||||
cmd/
|
||||
├── root.go
|
||||
├── list.go
|
||||
├── replace.go
|
||||
├── remove.go
|
||||
└── undo.go
|
||||
|
||||
internal/
|
||||
├── filters/
|
||||
├── history/
|
||||
├── listing/
|
||||
├── output/
|
||||
├── remove/
|
||||
├── replace/
|
||||
├── traversal/
|
||||
└── extension/ # new package for extension normalization engine
|
||||
|
||||
scripts/
|
||||
├── smoke-test-remove.sh
|
||||
└── smoke-test-replace.sh
|
||||
|
||||
tests/
|
||||
├── contract/
|
||||
│ ├── remove_command_ledger_test.go
|
||||
│ ├── remove_command_preview_test.go
|
||||
│ ├── replace_command_test.go
|
||||
│ └── (new) extension_command_test.go
|
||||
├── integration/
|
||||
│ ├── remove_flow_test.go
|
||||
│ ├── remove_undo_test.go
|
||||
│ ├── remove_validation_test.go
|
||||
│ └── replace_flow_test.go
|
||||
└── fixtures/ # shared test inputs
|
||||
```
|
||||
|
||||
**Structure Decision**: Maintain the single CLI project layout, adding an `internal/extension` package plus contract/integration tests mirroring existing replace/remove coverage.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| _None_ | — | — |
|
||||
28
specs/004-extension-rename/quickstart.md
Normal file
28
specs/004-extension-rename/quickstart.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Quickstart – Extension Normalization Command
|
||||
|
||||
1. **Preview extension changes.**
|
||||
```bash
|
||||
renamer extension .jpeg .JPG .jpg --dry-run
|
||||
```
|
||||
- Shows original → proposed paths.
|
||||
- Highlights conflicts or “no change” rows for files already ending in `.jpg`.
|
||||
|
||||
2. **Include nested directories or hidden assets when needed.**
|
||||
```bash
|
||||
renamer extension .yaml .yml .yml --recursive --hidden --dry-run
|
||||
```
|
||||
- `--recursive` traverses subdirectories.
|
||||
- `--hidden` opt-in keeps hidden files in scope.
|
||||
|
||||
3. **Apply changes after confirming preview.**
|
||||
```bash
|
||||
renamer extension .jpeg .JPG .jpg --yes
|
||||
```
|
||||
- `--yes` auto-confirms preview results.
|
||||
- Command exits `0` even if no files matched (prints “no candidates found”).
|
||||
|
||||
4. **Undo the most recent batch if needed.**
|
||||
```bash
|
||||
renamer undo
|
||||
```
|
||||
- Restores original extensions using `.renamer` ledger entry.
|
||||
17
specs/004-extension-rename/research.md
Normal file
17
specs/004-extension-rename/research.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Phase 0 Research – Extension Command
|
||||
|
||||
## Decision: Reuse Replace Engine Structure for Extension Normalization
|
||||
- **Rationale**: The replace command already supports preview/apply workflows, ledger logging, and shared traversal flags. By reusing its service abstractions (scope resolution → rule application → preview), we minimize new surface area while ensuring compliance with preview-first safety.
|
||||
- **Alternatives considered**: Building a standalone engine dedicated to extensions was rejected because it would duplicate scope traversal, preview formatting, and ledger writing logic, increasing maintenance and divergence risk.
|
||||
|
||||
## Decision: Normalize Extensions Using Case-Insensitive Matching
|
||||
- **Rationale**: Filesystems differ in case sensitivity; normalizing via `strings.EqualFold` (or equivalent) ensures consistent behavior regardless of platform, aligning with the spec’s clarification and reducing surprise for users migrating mixed-case assets.
|
||||
- **Alternatives considered**: Relying on filesystem semantics was rejected because it would produce divergent behavior (e.g., Linux vs. macOS). Requiring exact-case matches was rejected for being unfriendly to legacy archives with mixed casing.
|
||||
|
||||
## Decision: Record Detailed Extension Metadata in Ledger Entries
|
||||
- **Rationale**: Persisting the original extension list, target extension, and per-file before/after paths in ledger metadata keeps undo operations auditable and allows future analytics (e.g., reporting normalized counts). Existing ledger schema supports additional metadata fields without incompatible changes.
|
||||
- **Alternatives considered**: Storing only file path mappings was rejected because it obscures which extensions were targeted, hindering debugging. Creating a new ledger file was rejected for complicating undo logic.
|
||||
|
||||
## Decision: Extend Cobra CLI with Positional Arguments for Extensions
|
||||
- **Rationale**: Cobra natural handling of positional args enables `renamer extension [sources...] [target]`. Using argument parsing consistent with replace/remove reduces UX learning curve, and Cobra’s validation hooks simplify enforcing leading-dot requirements.
|
||||
- **Alternatives considered**: Introducing new flags (e.g., `--sources`, `--target`) was rejected because it diverges from existing command patterns and complicates scripting. Using prompts was rejected due to automation needs.
|
||||
111
specs/004-extension-rename/spec.md
Normal file
111
specs/004-extension-rename/spec.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Feature Specification: Extension Command for Multi-Extension Normalization
|
||||
|
||||
**Feature Branch**: `004-extension-rename`
|
||||
**Created**: 2025-10-29
|
||||
**Status**: Draft
|
||||
**Input**: User description: "实现扩展名修改(Extension)命令,类似于 replace 命令,可以支持把多个扩展名更改为一个指定的扩展名"
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2025-10-30
|
||||
- Q: Should extension comparisons treat casing uniformly or follow the host filesystem? → A: Always case-insensitive
|
||||
- Q: How should hidden files be handled when `--hidden` is omitted? → A: Exclude hidden entries
|
||||
- Q: What exit behavior should occur when no files match the given extensions? → A: Exit 0 with notice
|
||||
- Q: How should files that already have the target extension be represented? → A: Preview as no-change; skip on apply
|
||||
|
||||
## User Scenarios & Testing _(mandatory)_
|
||||
|
||||
### User Story 1 - Normalize Legacy Extensions in Bulk (Priority: P1)
|
||||
|
||||
As a power user cleaning project assets, I want a command to replace multiple file extensions with a single standardized extension so that I can align legacy files (e.g., `.jpeg`, `.JPG`) without hand-editing each one.
|
||||
|
||||
**Why this priority**: Delivers the core business value—fast extension normalization across large folders.
|
||||
|
||||
**Independent Test**: In a sample directory containing `.jpeg`, `.JPG`, and `.png`, run `renamer extension .jpeg .JPG .png .jpg --dry-run`, verify preview shows the new `.jpg` extension for each, then apply with `--yes` and confirm filesystem updates.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** files with extensions `.jpeg` and `.JPG`, **When** the user runs `renamer extension .jpeg .JPG .jpg`, **Then** the preview lists each file with the `.jpg` extension and apply renames successfully.
|
||||
2. **Given** nested directories, **When** the user adds `--recursive`, **Then** all matching extensions in subdirectories are normalized while unrelated files remain untouched.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Automation-Friendly Extension Updates (Priority: P2)
|
||||
|
||||
As an operator integrating renamer into CI scripts, I want deterministic exit codes, ledger entries, and undo support when changing extensions so automated jobs remain auditable and recoverable.
|
||||
|
||||
**Why this priority**: Ensures enterprise workflows can rely on extension updates without risking data loss.
|
||||
|
||||
**Independent Test**: Run `renamer extension .yaml .yml .yml --yes --path ./fixtures`, verify exit code `0`, inspect `.renamer` ledger for recorded operations, and confirm `renamer undo` restores originals.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a non-interactive environment with `--yes`, **When** the command completes without conflicts, **Then** exit code is `0` and ledger metadata captures original extension list and target extension.
|
||||
2. **Given** a ledger entry exists, **When** `renamer undo` runs, **Then** all files revert to their prior extensions even if the command was executed by automation.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Validate Extension Inputs and Conflicts (Priority: P3)
|
||||
|
||||
As a user preparing an extension migration, I want validation and preview warnings for invalid tokens, duplicate target names, and no-op operations so I can adjust before committing changes.
|
||||
|
||||
**Why this priority**: Reduces support load from misconfigured commands and protects against accidental overwrites.
|
||||
|
||||
**Independent Test**: Run `renamer extension .mp3 .MP3 mp3 --dry-run`, confirm validation fails because tokens must include leading `.`, and run a scenario where resulting filenames collide to ensure conflicts abort the apply step.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** invalid input (e.g., missing leading `.` or fewer than two arguments), **When** the command executes, **Then** it exits with non-zero status and prints actionable guidance.
|
||||
2. **Given** two files that would become the same path after extension normalization, **When** the preview runs, **Then** conflicts are listed and the apply step refuses to proceed until resolved.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- How does the rename plan surface conflicts when multiple files map to the same normalized extension?
|
||||
- When the target extension already matches some files, they appear in preview with a “no change” indicator and are skipped during apply without raising errors.
|
||||
- Hidden files and directories remain excluded unless the user supplies `--hidden`.
|
||||
- When no files match in preview or apply, the command must surface a “no candidates found” notice while completing successfully.
|
||||
|
||||
## Requirements _(mandatory)_
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: CLI MUST provide a dedicated `extension` subcommand that accepts one or more source extensions and a single target extension as positional arguments.
|
||||
- **FR-002**: Preview → confirm workflow MUST mirror existing commands: list original paths, proposed paths, and highlight extension changes before apply.
|
||||
- **FR-003**: Executions MUST append ledger entries capturing original extension list, target extension, affected files, and timestamps to support undo.
|
||||
- **FR-004**: Users MUST be able to undo the most recent extension 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`) consistent with `list` and `replace`, excluding hidden files and directories unless `--hidden` is explicitly supplied.
|
||||
- **FR-006**: Extension parsing MUST require leading `.` tokens, deduplicate case-insensitively, warn when duplicates or no-op tokens are supplied, and compare file extensions case-insensitively across all platforms.
|
||||
- **FR-007**: Preview MUST detect target conflicts (two files mapping to the same new path) and block apply until conflicts are resolved.
|
||||
- **FR-008**: Invalid invocations (e.g., fewer than two arguments, empty tokens after trimming) MUST exit with non-zero status and provide remediation tips.
|
||||
- **FR-009**: Help output MUST clearly explain argument order, sequential evaluation rules, and interaction with scope flags.
|
||||
- **FR-010**: When no files match the provided extensions, preview and apply runs MUST emit a clear “no candidates found” message and exit with status `0`.
|
||||
- **FR-011**: Preview MUST surface already-targeted files with a “no change” marker, and apply MUST skip them while returning success.
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **ExtensionRequest**: Working directory, ordered source extension list, target extension, scope flags, dry-run/apply settings.
|
||||
- **ExtensionSummary**: Totals for candidates, changed files, per-extension match counts, conflicts, and warning messages used for preview and ledger metadata.
|
||||
|
||||
## Success Criteria _(mandatory)_
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Users normalize 500 files’ extensions (preview + apply) in under 2 minutes end-to-end.
|
||||
- **SC-002**: 95% of beta testers correctly supply arguments (source extensions + target) after reading `renamer extension --help` without additional guidance.
|
||||
- **SC-003**: Automated regression tests confirm extension change + undo leave the filesystem unchanged in 100% of scripted scenarios.
|
||||
- **SC-004**: Support tickets related to inconsistent file extensions drop by 30% within the first release cycle after launch.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Command name will be `renamer extension` to align with existing verb-noun conventions.
|
||||
- Source extensions are literal matches with leading dots; wildcard or regex patterns remain out of scope.
|
||||
- Target extension must include a leading dot and is applied case-sensitively as provided.
|
||||
- Existing traversal, summary, and ledger infrastructure can be extended from the replace/remove commands.
|
||||
|
||||
## Dependencies & Risks
|
||||
|
||||
- Requires new extension-specific packages analogous to replace/remove for parser, engine, summary, and CLI wiring.
|
||||
- Help/quickstart documentation must be updated to explain argument order and extension validation.
|
||||
- Potential filename conflicts after normalization must be detected pre-apply to avoid overwriting files.
|
||||
160
specs/004-extension-rename/tasks.md
Normal file
160
specs/004-extension-rename/tasks.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Tasks: Extension Command for Multi-Extension Normalization
|
||||
|
||||
**Input**: Design documents from `/specs/004-extension-rename/`
|
||||
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/
|
||||
|
||||
**Tests**: Contract and integration tests are included because the specification mandates deterministic previews, ledger-backed undo, and automation safety.
|
||||
|
||||
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Establish initial package and CLI scaffolding.
|
||||
|
||||
- [X] T001 Create package doc stub for extension engine in `internal/extension/doc.go`
|
||||
- [X] T002 Register placeholder Cobra subcommand in `cmd/extension.go` and wire it into `cmd/root.go`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Core data structures and utilities required by all user stories.
|
||||
|
||||
- [X] T003 Define `ExtensionRequest` parsing helpers in `internal/extension/request.go`
|
||||
- [X] T004 Define `ExtensionSummary`, preview statuses, and metadata container in `internal/extension/summary.go`
|
||||
- [X] T005 Implement case-insensitive normalization and dedup helpers in `internal/extension/normalize.go`
|
||||
|
||||
**Checkpoint**: Foundation ready — user story implementation can now begin.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 – Normalize Legacy Extensions in Bulk (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Provide preview and apply flows that replace multiple source extensions with a single target extension across the scoped filesystem.
|
||||
|
||||
**Independent Test**: In a directory containing mixed `.jpeg`, `.JPG`, and `.png` files, run `renamer extension .jpeg .JPG .png .jpg --dry-run` to inspect preview output, then run with `--yes` to confirm filesystem updates.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [X] T010 [P] [US1] Add preview/apply contract coverage in `tests/contract/extension_command_test.go`
|
||||
- [X] T011 [P] [US1] Add normalization happy-path integration flow in `tests/integration/extension_flow_test.go`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [X] T006 [US1] Implement scoped candidate discovery and planning in `internal/extension/engine.go`
|
||||
- [X] T007 [US1] Render deterministic preview entries with change/no-change markers in `internal/extension/preview.go`
|
||||
- [X] T008 [US1] Apply planned renames with filesystem operations in `internal/extension/apply.go`
|
||||
- [X] T009 [US1] Wire Cobra command to preview/apply pipeline with scope flags in `cmd/extension.go`
|
||||
|
||||
**Checkpoint**: User Story 1 functional and independently testable.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 – Automation-Friendly Extension Updates (Priority: P2)
|
||||
|
||||
**Goal**: Ensure ledger entries, exit codes, and undo support enable scripted execution without manual intervention.
|
||||
|
||||
**Independent Test**: Execute `renamer extension .yaml .yml .yml --yes --path ./fixtures`, verify exit code `0`, inspect `.renamer` ledger for metadata, then run `renamer undo` to restore originals.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [X] T015 [P] [US2] Extend contract tests to verify ledger metadata and exit codes in `tests/contract/extension_ledger_test.go`
|
||||
- [X] T016 [P] [US2] Add automation/undo integration scenario in `tests/integration/extension_undo_test.go`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [X] T012 [US2] Persist extension-specific metadata during apply in `internal/extension/apply.go`
|
||||
- [X] T013 [US2] Ensure undo and CLI output handle extension batches in `cmd/undo.go`
|
||||
- [X] T014 [US2] Guarantee deterministic exit codes and non-match notices in `cmd/extension.go`
|
||||
|
||||
**Checkpoint**: User Stories 1 and 2 functional and independently testable.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 – Validate Extension Inputs and Conflicts (Priority: P3)
|
||||
|
||||
**Goal**: Provide robust input validation and conflict detection to prevent unsafe applies.
|
||||
|
||||
**Independent Test**: Run `renamer extension .mp3 .MP3 mp3 --dry-run` to confirm validation failure for missing dot, and run a collision scenario to verify preview conflicts block apply.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [X] T020 [P] [US3] Add validation and conflict contract coverage in `tests/contract/extension_validation_test.go`
|
||||
- [X] T021 [P] [US3] Add conflict-blocking integration scenario in `tests/integration/extension_validation_test.go`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [X] T017 [US3] Implement CLI argument validation and error messaging in `internal/extension/parser.go`
|
||||
- [X] T018 [US3] Detect conflicting target paths and accumulate preview warnings in `internal/extension/conflicts.go`
|
||||
- [X] T019 [US3] Surface validation failures and conflict gating in `cmd/extension.go`
|
||||
|
||||
**Checkpoint**: All user stories functional with independent validation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Documentation, tooling, and quality improvements spanning multiple stories.
|
||||
|
||||
- [X] T022 Update CLI flag documentation for extension command in `docs/cli-flags.md`
|
||||
- [X] T023 Add smoke test script covering extension preview/apply/undo in `scripts/smoke-test-extension.sh`
|
||||
- [X] T024 Run gofmt and `go test ./...` from repository root `./`
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
1. **Setup (Phase 1)** → primes new package and CLI wiring.
|
||||
2. **Foundational (Phase 2)** → must complete before any user story work.
|
||||
3. **User Story Phases (3–5)** → execute in priority order (P1 → P2 → P3) once foundational tasks finish.
|
||||
4. **Polish (Phase 6)** → after desired user stories are complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1 (P1)** → depends on Phase 2 completion; delivers MVP.
|
||||
- **US2 (P2)** → depends on US1 groundwork (ledger metadata builds atop apply logic).
|
||||
- **US3 (P3)** → depends on US1 preview/apply pipeline; validation hooks extend existing command.
|
||||
|
||||
### Task Dependencies (Selected)
|
||||
|
||||
- T006 depends on T003–T005.
|
||||
- T007, T008 depend on T006.
|
||||
- T009 depends on T006–T008.
|
||||
- T012 depends on T008.
|
||||
- T013, T014 depend on T012.
|
||||
- T017 depends on T003, T005.
|
||||
- T018 depends on T006–T007.
|
||||
- T019 depends on T017–T018.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
- **Within US1**: After T009, tasks T010 and T011 can run in parallel to add contract and integration coverage.
|
||||
- **Across Stories**: Once US1 implementation (T006–T009) is complete, US2 test tasks T015 and T016 can proceed in parallel while T012–T014 are under development.
|
||||
- **Validation Work**: For US3, T020 and T021 can execute in parallel after T019 ensures CLI gating is wired.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First
|
||||
|
||||
1. Complete Phases 1–2 to establish scaffolding and data structures.
|
||||
2. Deliver Phase 3 (US1) to unlock core extension normalization (MVP).
|
||||
3. Validate with contract/integration tests (T010, T011) and smoke through Quickstart scenario.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. After MVP, implement Phase 4 (US2) to add ledger/undo guarantees for automation.
|
||||
2. Follow with Phase 5 (US3) to harden validation and conflict handling.
|
||||
3. Finish with Phase 6 polish tasks for documentation and operational scripts.
|
||||
|
||||
### Parallel Approach
|
||||
|
||||
1. One developer completes Phases 1–2.
|
||||
2. Parallelize US1 implementation (engine vs. CLI vs. tests) once foundations are ready.
|
||||
3. Assign US2 automation tasks to a second developer after US1 apply logic stabilizes.
|
||||
4. Run US3 validation tasks concurrently with Polish updates once US2 nears completion.
|
||||
Reference in New Issue
Block a user