Add extension normalization command

This commit is contained in:
Rogee
2025-10-30 10:31:53 +08:00
parent f66c59fd57
commit 6a353b5086
35 changed files with 2306 additions and 2 deletions

View 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.

View 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'

View 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.

View 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_ | — | — |

View 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.

View 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 specs 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 Cobras 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.

View 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.

View 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 (35)** → 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 T003T005.
- T007, T008 depend on T006.
- T009 depends on T006T008.
- T012 depends on T008.
- T013, T014 depend on T012.
- T017 depends on T003, T005.
- T018 depends on T006T007.
- T019 depends on T017T018.
---
## 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 (T006T009) is complete, US2 test tasks T015 and T016 can proceed in parallel while T012T014 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 12 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 12.
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.