# Bug Investigation: Issues #2, #3, and #4

**Investigation Date:** 2026-02-03
**Investigator:** Research Agent
**Repository:** gitflow-analytics
**Status:** Root causes identified

---

## Executive Summary

Investigated three open bugs in gitflow-analytics related to configuration handling:

1. **Issue #2**: Author exclusions not filtering commits from reports
2. **Issue #3**: JIRA story points field extraction failing
3. **Issue #4**: aliases.yaml loading 0 aliases due to format mismatch

All three issues have been root-caused with specific file locations, line numbers, and recommended fix approaches.

---

## Issue #2: exclude.authors Config Not Filtering Commits

### Expected Behavior
Authors listed in `analysis.exclude.authors` configuration should be filtered from all reports (CSV, markdown, etc.)

### Actual Behavior
All authors appear in reports regardless of exclusion configuration

### Root Cause Analysis

**Location:** `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/reports/base.py`
**Lines:** 241-311 (specifically lines 291-294)

#### The Problem

The `_should_exclude_author()` method in `BaseReportGenerator` checks for author exclusions in the following order:

```python
def _should_exclude_author(self, commit: Dict[str, Any], excluded_lower: List[str]) -> bool:
    # Check canonical_id first
    canonical_id = commit.get("canonical_id", "")
    if canonical_id and canonical_id.lower() in excluded_lower:
        return True

    # Check other identity fields
    for field in ["author_email", "author_name", "author"]:
        value = commit.get(field, "")
        if value and value.lower() in excluded_lower:
            return True

    # Check for bot patterns
    # ...
```

**The Issue:** The exclusion filter expects `canonical_id` to be present in commit dictionaries, but **commits fetched from the database may not have this field populated**.

#### Evidence

1. **Configuration loading works correctly** (lines 591-593 in `config/loader.py`):
   ```python
   exclude_authors=analysis_data.get("exclude", {}).get(
       "authors", ["dependabot[bot]", "renovate[bot]"]
   )
   ```

2. **Report generators receive the exclusion list** (lines 3490, 3495 in `cli.py`):
   ```python
   report_gen = CSVReportGenerator(
       anonymize=anonymize or cfg.output.anonymize_enabled,
       exclude_authors=cfg.analysis.exclude_authors,  # ✓ Passed correctly
       identity_resolver=identity_resolver,
   )
   ```

3. **Filter is applied in pre_process** (line 219 in `base.py`):
   ```python
   def pre_process(self, data: ReportData) -> ReportData:
       # Apply author exclusions if configured
       if self.exclude_authors:
           data = self._filter_excluded_authors(data)  # ✓ Called correctly
   ```

4. **But the field check fails** (line 292 in `base.py`):
   ```python
   canonical_id = commit.get("canonical_id", "")  # ✗ May be empty or None
   if canonical_id and canonical_id.lower() in excluded_lower:
       return True
   ```

#### Why It Fails

When commits are loaded from the database or cache, the `canonical_id` field may be:
- Not set (returns empty string from `.get()`)
- Set to None
- Present but not matching the exclusion list format

The fallback checks for `author_email`, `author_name`, and `author` may also fail if:
- The exclusion config uses email format: `["bot@example.com"]`
- But the commit dict uses a different key structure
- Or the identity resolution has already normalized the email

### Recommended Fix

**Approach 1: Ensure canonical_id is Always Populated** (Recommended)

Modify the commit data preparation pipeline to ensure `canonical_id` is always present:

```python
# In the data fetching layer (database.py or analyzer.py)
def prepare_commit_for_report(commit_data: Dict[str, Any], identity_resolver) -> Dict[str, Any]:
    """Ensure commit has canonical_id for filtering."""
    if "canonical_id" not in commit_data or not commit_data["canonical_id"]:
        # Resolve identity
        if identity_resolver:
            canonical_id = identity_resolver.resolve(commit_data.get("author_email", ""))
            commit_data["canonical_id"] = canonical_id
        else:
            # Fallback to author_email
            commit_data["canonical_id"] = commit_data.get("author_email", "unknown")
    return commit_data
```

**Approach 2: Make Exclusion Check More Robust** (Quick Fix)

Update `_should_exclude_author()` to handle missing canonical_id gracefully:

```python
def _should_exclude_author(self, commit: Dict[str, Any], excluded_lower: List[str]) -> bool:
    """Check if a commit author should be excluded."""
    # Build list of identity values to check
    identity_values = []

    # Add canonical_id if present
    canonical_id = commit.get("canonical_id")
    if canonical_id:
        identity_values.append(canonical_id.lower())

    # Add other identity fields
    for field in ["author_email", "author_name", "author"]:
        value = commit.get(field)
        if value:
            identity_values.append(str(value).lower())

    # Check all identity values against exclusion list
    for identity_value in identity_values:
        if identity_value in excluded_lower:
            return True

    # Check for bot patterns
    author_name = commit.get("author_name", "").lower()
    author_email = commit.get("author_email", "").lower()

    bot_indicators = ["[bot]", "bot@", "-bot", "_bot", ".bot"]
    for indicator in bot_indicators:
        if indicator in author_name or indicator in author_email:
            return True

    return False
```

**Approach 3: Add Debug Logging** (For Troubleshooting)

Add debug output to understand what's happening:

```python
def _should_exclude_author(self, commit: Dict[str, Any], excluded_lower: List[str]) -> bool:
    """Check if a commit author should be excluded."""
    canonical_id = commit.get("canonical_id", "")

    # DEBUG: Log what we're checking
    if self.logger.isEnabledFor(logging.DEBUG):
        self.logger.debug(
            f"Checking exclusion for commit: "
            f"canonical_id={canonical_id}, "
            f"author_email={commit.get('author_email')}, "
            f"excluded_list={excluded_lower}"
        )

    # ... rest of the method
```

### Testing Recommendations

1. Add unit test with commits missing `canonical_id`
2. Add integration test that verifies exclusion filtering
3. Test with various config formats:
   - Email-based: `["bot@example.com"]`
   - Name-based: `["Dependabot"]`
   - Pattern-based: `["dependabot[bot]"]`

---

## Issue #3: JIRA Story Points Not Fetched

### Expected Behavior
Story points from JIRA custom fields (configured in `pm_integration.platforms.jira.config.story_point_fields`) should be captured in `jira_tickets.db`

### Actual Behavior
`jira_tickets.db` has 0 rows with story points populated

### Root Cause Analysis

**Location:** `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/pm_framework/adapters/jira_adapter.py`
**Lines:** 1395-1421 (specifically method `_extract_story_points`)

#### The Problem

The JIRA adapter's `_extract_story_points()` method **correctly implements** story point extraction logic:

```python
def _extract_story_points(self, fields: dict[str, Any]) -> Optional[int]:
    """Extract story points from JIRA custom fields."""
    # Try configured story point fields first
    for field_id in self.story_point_fields:
        if field_id in fields and fields[field_id] is not None:
            value = fields[field_id]
            try:
                if isinstance(value, (int, float)):
                    return int(value)
                elif isinstance(value, str) and value.strip():
                    return int(float(value.strip()))
            except (ValueError, TypeError):
                continue

    # Use base class method as fallback
    return super()._extract_story_points(fields)
```

**However**, the issue is likely one of the following:

#### Scenario 1: Incorrect Field IDs in Configuration (Most Likely)

**Problem:** The configured `story_point_fields` don't match the actual JIRA custom field IDs.

**Default configuration** (lines 572-581):
```python
self.story_point_fields = config.get(
    "story_point_fields",
    [
        "customfield_10016",  # Common JIRA Cloud story points field
        "customfield_10021",  # Alternative field
        "customfield_10002",  # Another common ID
        "Story Points",       # Field name fallback
        "storypoints",        # Alternative name
    ],
)
```

**Why this fails:**
- JIRA custom field IDs are **organization-specific**
- The default IDs may not match the user's JIRA instance
- Field discovery is implemented but the user config may override with wrong IDs

#### Scenario 2: Fields Not Requested in API Call

**Problem:** The JIRA API call may not request custom fields.

**Evidence** (lines 959-961):
```python
response = session.get(
    f"{self.base_url}/rest/api/3/search/jql",
    params={
        "jql": jql,
        "startAt": start_at,
        "maxResults": self.batch_size,
        "fields": "*all",  # ✓ Requests all fields
        "expand": "changelog,renderedFields",
    },
)
```

This looks correct - `"fields": "*all"` should include custom fields.

#### Scenario 3: Field Discovery Not Used

**Problem:** Field discovery exists but isn't used to validate configuration.

**Evidence** (lines 1135-1189):
```python
def _discover_fields(self) -> None:
    """Discover and cache JIRA field mappings."""
    # ... discovers fields ...

    for field in fields:
        field_id = field.get("id", "")
        field_name = field.get("name", "").lower()

        # Identify potential story point fields
        if any(term in field_name for term in ["story", "point", "estimate", "size"]):
            story_point_candidates.append((field_id, field.get("name", "")))

    self.logger.info(f"Discovered {len(fields)} JIRA fields")

    if story_point_candidates:
        self.logger.info("Potential story point fields found:")
        for field_id, field_name in story_point_candidates[:5]:
            self.logger.info(f"  {field_id}: {field_name}")
```

Field discovery **logs** potential fields but **doesn't automatically use them** to override configuration.

### Recommended Fix

**Approach 1: Auto-Detect Story Point Fields** (Recommended)

Modify the JIRA adapter to automatically detect and use story point fields:

```python
def _discover_fields(self) -> None:
    """Discover and cache JIRA field mappings."""
    try:
        # ... existing field discovery code ...

        # Auto-detect story point fields if none are configured
        if story_point_candidates and not self.story_point_fields:
            self.logger.info("No story point fields configured, using auto-detected fields")
            self.story_point_fields = [field_id for field_id, _ in story_point_candidates]

        # Validate configured fields against discovered fields
        elif self.story_point_fields:
            discovered_field_ids = {f["id"] for f in fields}
            invalid_fields = [
                field for field in self.story_point_fields
                if field.startswith("customfield_") and field not in discovered_field_ids
            ]

            if invalid_fields:
                self.logger.warning(
                    f"Configured story point fields not found in JIRA: {invalid_fields}"
                )
                self.logger.warning("Available story point candidates:")
                for field_id, field_name in story_point_candidates[:5]:
                    self.logger.warning(f"  - {field_id}: {field_name}")

        # ... rest of method ...
```

**Approach 2: Add Configuration Validation Command**

Add a CLI command to validate JIRA configuration:

```python
@cli.command(name="validate-jira")
@click.option("--config", required=True, help="Path to config file")
def validate_jira_config(config: str):
    """Validate JIRA integration configuration.

    Checks that configured custom field IDs exist in the JIRA instance.
    """
    from .pm_framework.adapters.jira_adapter import JIRAAdapter

    # Load config
    cfg = load_config(Path(config))

    # Initialize JIRA adapter
    jira_config = cfg.pm_integration.platforms["jira"].config
    adapter = JIRAAdapter(jira_config)

    # Authenticate
    if not adapter.authenticate():
        click.echo("❌ JIRA authentication failed")
        return

    # Discover fields
    adapter._discover_fields()

    # Check configured story point fields
    story_point_fields = jira_config.get("story_point_fields", [])
    discovered_fields = adapter._field_mapping

    click.echo("\n📊 Story Point Field Validation:")
    for field_id in story_point_fields:
        if field_id in discovered_fields:
            field_name = discovered_fields[field_id]["name"]
            click.echo(f"  ✓ {field_id}: {field_name}")
        else:
            click.echo(f"  ❌ {field_id}: NOT FOUND")

    # Show available story point candidates
    click.echo("\n💡 Available Story Point Field Candidates:")
    # ... list candidates ...
```

**Approach 3: Add Diagnostic Logging**

Add detailed logging to track field extraction:

```python
def _extract_story_points(self, fields: dict[str, Any]) -> Optional[int]:
    """Extract story points from JIRA custom fields."""
    self.logger.debug(f"Attempting to extract story points from {len(fields)} fields")
    self.logger.debug(f"Configured story point fields: {self.story_point_fields}")

    # Try configured story point fields first
    for field_id in self.story_point_fields:
        if field_id in fields:
            value = fields[field_id]
            self.logger.debug(f"Found field {field_id} with value: {value} (type: {type(value)})")

            if value is not None:
                try:
                    if isinstance(value, (int, float)):
                        result = int(value)
                        self.logger.info(f"✓ Extracted story points: {result} from {field_id}")
                        return result
                    elif isinstance(value, str) and value.strip():
                        result = int(float(value.strip()))
                        self.logger.info(f"✓ Extracted story points: {result} from {field_id}")
                        return result
                except (ValueError, TypeError) as e:
                    self.logger.warning(f"Failed to parse story points from {field_id}: {e}")
                    continue
        else:
            self.logger.debug(f"Field {field_id} not present in issue fields")

    # Log when no story points found
    self.logger.debug("No story points found in configured fields, trying base class fallback")
    result = super()._extract_story_points(fields)

    if result:
        self.logger.info(f"✓ Extracted story points: {result} via base class fallback")
    else:
        self.logger.warning("❌ No story points found in any configured fields")

    return result
```

### Testing Recommendations

1. Add environment variable to enable JIRA field debugging: `GITFLOW_JIRA_DEBUG=1`
2. Test field discovery with multiple JIRA instances
3. Add unit tests with various custom field formats:
   - Integer values: `{"customfield_10016": 5}`
   - Float values: `{"customfield_10016": 5.0}`
   - String values: `{"customfield_10016": "5"}`
   - Missing fields: `{}`
4. Add integration test that validates story points are cached correctly

---

## Issue #4: aliases.yaml Loads 0 Aliases

### Expected Behavior
Aliases defined in `aliases.yaml` should be loaded and available for identity resolution

### Actual Behavior
`aliases.py` loads 0 aliases despite file containing alias definitions

### Root Cause Analysis

**Location:** `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/config/aliases.py`
**Lines:** 100-129 (specifically lines 104-120)

#### The Problem

The `AliasesManager.load()` method expects a **specific YAML format**:

```python
def load(self) -> None:
    """Load aliases from file."""
    try:
        with open(self.aliases_path) as f:
            data = yaml.safe_load(f) or {}

        self.aliases = []
        for alias_data in data.get("developer_aliases", []):  # ← Expects "developer_aliases"
            # Support both 'primary_email' (new) and 'canonical_email' (old)
            primary_email = alias_data.get("primary_email") or alias_data.get("canonical_email")

            if not primary_email:
                logger.warning(f"Skipping alias entry without primary_email: {alias_data}")
                continue

            self.aliases.append(
                DeveloperAlias(
                    primary_email=primary_email,
                    aliases=alias_data.get("aliases", []),
                    name=alias_data.get("name"),
                    # ...
                )
            )
```

**Expected format:**
```yaml
developer_aliases:
  - primary_email: john@company.com
    aliases:
      - jdoe@gmail.com
      - john.doe@personal.com
    name: John Doe
  - primary_email: jane@company.com
    aliases:
      - jane.smith@gmail.com
    name: Jane Smith
```

**But users likely have this format:**
```yaml
teams:
  - name: Engineering
    members:
      - primary_email: john@company.com
        aliases:
          - jdoe@gmail.com
        name: John Doe
      - primary_email: jane@company.com
        aliases:
          - jane.smith@gmail.com
        name: Jane Smith
```

#### Why It Fails

1. The loader looks for `data.get("developer_aliases", [])` (line 104)
2. If the user's YAML uses `teams` or any other top-level key, it returns an empty list `[]`
3. No aliases are loaded, resulting in "0 aliases loaded"

#### Evidence from Code

**File structure validation** (lines 100-129):
```python
data = yaml.safe_load(f) or {}  # Load YAML
for alias_data in data.get("developer_aliases", []):  # ← STRICT KEY REQUIREMENT
    # Process aliases
```

**No format validation or error reporting:**
- No check for common alternate formats
- No warning if YAML is valid but uses wrong structure
- Silent failure (returns 0 aliases with only a debug log)

**Save format shows expected structure** (lines 148-171):
```python
data = {
    "# Developer Identity Aliases": None,
    "# Generated by GitFlow Analytics": None,
    "# Share this file across multiple config files": None,
    "# Each alias maps multiple email addresses to a single developer": None,
    "developer_aliases": [alias.to_dict() for alias in self.aliases],  # ← EXPECTED KEY
}
```

### Recommended Fix

**Approach 1: Support Multiple YAML Formats** (Recommended)

Make the loader flexible to support common YAML structures:

```python
def load(self) -> None:
    """Load aliases from file.

    Supports multiple YAML formats:
    1. Direct format: developer_aliases: [...]
    2. Team format: teams: [{members: [...]}]
    3. Flat format: aliases: [...]
    """
    if not self.aliases_path or not self.aliases_path.exists():
        logger.debug("No aliases file found or path not set")
        return

    try:
        with open(self.aliases_path) as f:
            data = yaml.safe_load(f) or {}

        self.aliases = []

        # Try direct format first (expected format)
        alias_list = data.get("developer_aliases", [])

        # Try team format if direct format is empty
        if not alias_list and "teams" in data:
            logger.info("Detected team-based aliases format")
            alias_list = []
            for team in data["teams"]:
                if "members" in team:
                    alias_list.extend(team["members"])

        # Try flat format if still empty
        if not alias_list and "aliases" in data:
            logger.info("Detected flat aliases format")
            alias_list = data["aliases"]

        # Validate we found aliases
        if not alias_list:
            logger.warning(
                f"No aliases found in {self.aliases_path}. "
                f"Expected keys: 'developer_aliases', 'teams', or 'aliases'. "
                f"Found keys: {list(data.keys())}"
            )
            return

        # Process aliases (existing code)
        for alias_data in alias_list:
            primary_email = alias_data.get("primary_email") or alias_data.get("canonical_email")

            if not primary_email:
                logger.warning(f"Skipping alias entry without primary_email: {alias_data}")
                continue

            self.aliases.append(
                DeveloperAlias(
                    primary_email=primary_email,
                    aliases=alias_data.get("aliases", []),
                    name=alias_data.get("name"),
                    confidence=alias_data.get("confidence", 1.0),
                    reasoning=alias_data.get("reasoning", ""),
                )
            )

        logger.info(f"Loaded {len(self.aliases)} developer aliases from {self.aliases_path}")

    except yaml.YAMLError as e:
        logger.error(f"Error parsing aliases file {self.aliases_path}: {e}")
        raise
    except Exception as e:
        logger.error(f"Error loading aliases file {self.aliases_path}: {e}")
        raise
```

**Approach 2: Add Format Validation Command**

Add a CLI command to validate aliases.yaml format:

```python
@cli.command(name="validate-aliases")
@click.argument("aliases_file", type=click.Path(exists=True))
def validate_aliases(aliases_file: str):
    """Validate aliases.yaml file format.

    Checks that the file uses the correct structure and provides
    guidance on fixing common format issues.
    """
    from .config.aliases import AliasesManager
    import yaml

    path = Path(aliases_file)

    # Load raw YAML
    with open(path) as f:
        data = yaml.safe_load(f) or {}

    click.echo(f"📄 Validating aliases file: {path}")
    click.echo(f"   Top-level keys: {list(data.keys())}")

    # Try loading with AliasesManager
    manager = AliasesManager(path)
    manager.load()

    click.echo(f"\n✅ Loaded {len(manager.aliases)} aliases")

    if len(manager.aliases) == 0:
        click.echo("\n⚠️  No aliases loaded. Common issues:")
        click.echo("   1. File should have 'developer_aliases' top-level key")
        click.echo("   2. Each alias needs 'primary_email' field")
        click.echo("   3. Check YAML indentation")
        click.echo("\nExpected format:")
        click.echo("""
developer_aliases:
  - primary_email: user@company.com
    aliases:
      - alias1@email.com
      - alias2@email.com
    name: User Name
        """)
    else:
        click.echo("\n📋 Loaded aliases:")
        for alias in manager.aliases:
            click.echo(f"   • {alias.primary_email}: {len(alias.aliases)} aliases")
```

**Approach 3: Improve Error Messages**

Add detailed logging when no aliases are found:

```python
def load(self) -> None:
    """Load aliases from file."""
    if not self.aliases_path or not self.aliases_path.exists():
        logger.debug("No aliases file found or path not set")
        return

    try:
        with open(self.aliases_path) as f:
            data = yaml.safe_load(f) or {}

        # Check if file is empty
        if not data:
            logger.warning(f"Aliases file {self.aliases_path} is empty or invalid YAML")
            return

        # Check for expected key
        if "developer_aliases" not in data:
            logger.error(
                f"❌ Aliases file missing 'developer_aliases' key. "
                f"Found keys: {list(data.keys())}. "
                f"Please check file format."
            )
            logger.error("Expected format:")
            logger.error("  developer_aliases:")
            logger.error("    - primary_email: user@company.com")
            logger.error("      aliases:")
            logger.error("        - alias@email.com")
            return

        self.aliases = []
        alias_list = data.get("developer_aliases", [])

        # Check if list is empty
        if not alias_list:
            logger.warning(
                f"'developer_aliases' key exists but is empty in {self.aliases_path}"
            )
            return

        # Process aliases
        skipped = 0
        for alias_data in alias_list:
            primary_email = alias_data.get("primary_email") or alias_data.get("canonical_email")

            if not primary_email:
                logger.warning(f"Skipping alias entry without primary_email: {alias_data}")
                skipped += 1
                continue

            self.aliases.append(
                DeveloperAlias(
                    primary_email=primary_email,
                    aliases=alias_data.get("aliases", []),
                    name=alias_data.get("name"),
                    confidence=alias_data.get("confidence", 1.0),
                    reasoning=alias_data.get("reasoning", ""),
                )
            )

        logger.info(
            f"✅ Loaded {len(self.aliases)} developer aliases from {self.aliases_path}"
        )
        if skipped > 0:
            logger.warning(f"⚠️  Skipped {skipped} invalid alias entries")

    except yaml.YAMLError as e:
        logger.error(f"❌ Error parsing aliases file {self.aliases_path}: {e}")
        raise
    except Exception as e:
        logger.error(f"❌ Error loading aliases file {self.aliases_path}: {e}")
        raise
```

### Testing Recommendations

1. Add unit tests with various YAML formats:
   - Direct format: `developer_aliases: [...]`
   - Team format: `teams: [{members: [...]}]`
   - Flat format: `aliases: [...]`
   - Invalid formats: missing keys, wrong types, etc.
2. Add example files in docs: `docs/examples/aliases-*.yaml`
3. Add schema validation using `jsonschema` or `pydantic`
4. Test with malformed YAML to ensure clear error messages

---

## Summary of Findings

| Issue | Root Cause | Fix Priority | Estimated Effort |
|-------|-----------|--------------|------------------|
| #2: Author exclusions | `canonical_id` field not populated in commits | High | Medium |
| #3: JIRA story points | Custom field IDs don't match JIRA instance | High | Low |
| #4: Aliases format | Strict YAML format requirement | Medium | Low |

---

## Recommended Implementation Order

1. **Issue #4** (aliases.yaml) - Quickest fix, improves user experience
   - Add format validation with clear error messages
   - Support multiple YAML formats
   - Estimated time: 2-4 hours

2. **Issue #3** (JIRA story points) - Add diagnostic tooling
   - Add `validate-jira` command to check field configuration
   - Add detailed logging to field extraction
   - Estimated time: 3-5 hours

3. **Issue #2** (author exclusions) - Requires data pipeline changes
   - Ensure `canonical_id` is always populated
   - Make exclusion check more robust
   - Add integration tests
   - Estimated time: 4-6 hours

---

## Additional Recommendations

### General Code Quality

1. **Add Configuration Validation Layer**
   - Validate all config keys against expected schema
   - Provide clear error messages for common mistakes
   - Add `gitflow-analytics validate-config` command

2. **Improve Logging**
   - Add structured logging with clear prefixes (✅, ❌, ⚠️)
   - Use consistent log levels (DEBUG, INFO, WARNING, ERROR)
   - Add debug mode via environment variable

3. **Add Integration Tests**
   - Test complete data flow: DB → Filter → Report
   - Test JIRA field extraction with mock data
   - Test aliases loading with various formats

4. **Documentation Updates**
   - Document expected YAML formats with examples
   - Add troubleshooting guide for common configuration issues
   - Create migration guide for users with old formats

---

## Files Referenced

### Issue #2 (Author Exclusions)
- `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/reports/base.py` (lines 241-311)
- `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/config/loader.py` (lines 591-593)
- `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/config/schema.py` (lines 299)
- `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/cli.py` (lines 3490, 3495)

### Issue #3 (JIRA Story Points)
- `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/pm_framework/adapters/jira_adapter.py` (lines 572-581, 1135-1189, 1395-1421)

### Issue #4 (Aliases Format)
- `/Users/masa/Projects/gitflow-analytics/src/gitflow_analytics/config/aliases.py` (lines 100-129, 148-171)

---

**End of Investigation Report**
