Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

Mapping table (documented inline):
Type prefix rules:
- All paths under skills/ AND at least one new file (status A) → feat
- All paths under skills/ AND only modifications (status M) → fix
- All paths under skills/ AND at least one new file (status
first or second column is A, ?, R, or C) → feat
- All paths under skills/ AND only modifications (status M, D,
or space in both columns) → fix
- All paths under contexts/ or knowledge/ → docs
- All paths under agents/ AND new file → feat
- All paths under agents/ AND modification → fix
Expand Down Expand Up @@ -118,8 +120,9 @@ def derive_type(paths: list[str], git_statuses: list[str] | None = None) -> str:

Args:
paths: List of warehouse-relative path strings.
git_statuses: Optional list of single-char git status codes per path.
Use 'A' for new/added files, 'M' for modifications, '?' for untracked.
git_statuses: Optional list of two-character git porcelain status
codes per path (index column + worktree column). A file is
treated as "new" when either column is A, ?, R, or C.

Returns:
One of: 'feat', 'fix', 'docs', 'chore', 'refactor', 'test'.
Expand All @@ -143,10 +146,13 @@ def derive_type(paths: list[str], git_statuses: list[str] | None = None) -> str:
return "docs"

if top in ("skills", "agents"):
# Check statuses to distinguish feat vs fix
# Check statuses to distinguish feat vs fix.
# Porcelain is two chars: index column + worktree column.
# Treat as "new" when either column is A, ?, R, or C.
if git_statuses:
statuses_clean = [s.strip() for s in git_statuses]
has_new = any(s in ("A", "A ", " A", "??") for s in statuses_clean)
has_new = any(
any(ch in "A?RC" for ch in status[:2]) for status in git_statuses
)
else:
# Without status information, assume new files (feat)
has_new = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,64 @@ def test_tc8_empty_paths_raises(self):
# ─────────────────────────────────────────────────────────────────────────────


class TestCompoundPorcelainStatusCodes:
"""PER-185: compound two-char git status codes (AM, AD, MM, RM, etc.)."""

def test_am_added_then_modified_is_feat(self):
"""AM (added in index, modified in worktree) → feat."""
mod = _load_script()
result = mod.derive_type(
["skills/foo/SKILL.md"],
git_statuses=["AM"],
)
assert result == "feat"

def test_ad_added_then_deleted_is_feat(self):
"""AD (added in index, deleted in worktree) → feat."""
mod = _load_script()
result = mod.derive_type(
["skills/foo/SKILL.md"],
git_statuses=["AD"],
)
assert result == "feat"

def test_mm_modified_both_columns_is_fix(self):
"""MM (modified in index and worktree) → fix."""
mod = _load_script()
result = mod.derive_type(
["skills/foo/SKILL.md"],
git_statuses=["MM"],
)
assert result == "fix"

def test_rm_renamed_then_modified_is_feat(self):
"""RM (renamed in index, modified in worktree) → feat."""
mod = _load_script()
result = mod.derive_type(
["skills/foo/SKILL.md"],
git_statuses=["RM"],
)
assert result == "feat"

def test_worktree_deleted_is_fix(self):
"""' D' (deleted in worktree) → fix."""
mod = _load_script()
result = mod.derive_type(
["skills/foo/SKILL.md"],
git_statuses=[" D"],
)
assert result == "fix"

def test_index_deleted_is_fix(self):
"""'D ' (deleted in index) → fix."""
mod = _load_script()
result = mod.derive_type(
["skills/foo/SKILL.md"],
git_statuses=["D "],
)
assert result == "fix"


class TestDocumentedInvocationWithGitStatuses:
"""Tests for Finding 3 fix: SKILL.md documented invocation passes --git-statuses."""

Expand Down
Loading