mirror of
https://github.com/Jeffallan/claude-skills.git
synced 2026-09-14 18:43:38 +08:00
fix(skills): correct legacy-modernizer path in fastapi-expert; add ReferencePathChecker
A strict path audit following #225 found one more broken reference: fastapi-expert's migration-from-django.md cited an absolute-style path that resolves nowhere. To guard this recurring bug class, add ReferencePathChecker to validate-skills.py: every backtick or markdown-link .md path in skill files must resolve relative to the containing file or the skill root. Runs by default, so CI and make validate exercise it on every push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E6LA4sndtVGyvoXqwYeHoB
This commit is contained in:
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `ReferencePathChecker` in `scripts/validate-skills.py`: validates that file paths cited in skill markdown (backtick paths and markdown links) resolve relative to the containing file or the skill root. Broken paths previously failed silently when an agent tried to load deferred reference content; this class of bug has now recurred across several releases and is guarded automatically in CI and `make validate`
|
||||
|
||||
### Fixed
|
||||
- `vue-expert-js/SKILL.md`: three shared-Vue reference paths pointed at `vue-expert/references/*.md`, which does not resolve from the skill directory; corrected to `../vue-expert/references/*.md` (#225)
|
||||
- `react-expert/references/migration-class-to-modern.md`: self-referencing path `react-expert/references/server-components.md` corrected to `references/server-components.md` (#225)
|
||||
- `fastapi-expert/references/migration-from-django.md`: cross-reference to legacy-modernizer used an absolute-style path (`/skills/legacy-modernizer/...`) that resolves nowhere; corrected to `../legacy-modernizer/references/migration-strategies.md`. Found by the new `ReferencePathChecker` audit
|
||||
|
||||
### Contributors
|
||||
- @vasugarg09 — Fixed broken relative reference paths in `vue-expert-js` and `react-expert` (#225)
|
||||
|
||||
## [0.4.15] - 2026-05-20
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -254,6 +254,7 @@ The script validates:
|
||||
- **Name format** - Letters, numbers, hyphens only
|
||||
- **Description** - Max 1024 chars, must contain "Use when" trigger clause
|
||||
- **References** - Directory exists, has files, proper headers
|
||||
- **Reference paths** - File paths cited in skill markdown resolve relative to the containing file or the skill root
|
||||
- **Count consistency** - Skills/reference counts match across documentation
|
||||
|
||||
**Options:**
|
||||
|
||||
@@ -813,6 +813,62 @@ class NonStandardHeadersChecker(BaseChecker):
|
||||
return issues
|
||||
|
||||
|
||||
class ReferencePathChecker(BaseChecker):
|
||||
"""Validates that relative file paths cited in skill markdown resolve.
|
||||
|
||||
Skill files cite paths to deferred content in backticks (e.g.
|
||||
`references/testing.md` in routing tables) and markdown links. Agents
|
||||
resolve these relative to the containing file or the skill root; a path
|
||||
that resolves from neither base fails silently when an agent tries to
|
||||
load it. Cross-skill references must use the ../other-skill/ form so
|
||||
they resolve from those same bases.
|
||||
"""
|
||||
|
||||
name = "reference-paths"
|
||||
category = "references"
|
||||
|
||||
BACKTICK_REF = re.compile(r"`([^`\s]+\.md)`")
|
||||
MARKDOWN_LINK_REF = re.compile(r"\]\(([^)\s#]+\.md)(?:#[^)]*)?\)")
|
||||
FENCED_CODE_BLOCK = re.compile(r"^\s*```.*?^\s*```[^\n]*", re.MULTILINE | re.DOTALL)
|
||||
|
||||
def check(self, skill_path: Path, skill_name: str) -> list[ValidationIssue]:
|
||||
issues = []
|
||||
for md_file in sorted(skill_path.rglob("*.md")):
|
||||
text = md_file.read_text()
|
||||
# Fenced code blocks may cite hypothetical example paths; real
|
||||
# cross-references live in prose, bullets, and routing tables.
|
||||
prose = self.FENCED_CODE_BLOCK.sub("", text)
|
||||
refs = set(self.BACKTICK_REF.findall(prose)) | set(self.MARKDOWN_LINK_REF.findall(prose))
|
||||
lines = text.split("\n")
|
||||
for ref in sorted(refs):
|
||||
if self._is_exempt(ref):
|
||||
continue
|
||||
if (md_file.parent / ref).exists() or (skill_path / ref).exists():
|
||||
continue
|
||||
lineno = next((i for i, line in enumerate(lines, 1) if ref in line), None)
|
||||
location = f" (line {lineno})" if lineno else ""
|
||||
issues.append(
|
||||
ValidationIssue(
|
||||
skill=skill_name,
|
||||
check=self.name,
|
||||
severity=Severity.ERROR,
|
||||
message=f"Unresolvable file reference '{ref}'{location} - "
|
||||
"path must resolve relative to the containing file or the skill root",
|
||||
file=str(md_file),
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
@staticmethod
|
||||
def _is_exempt(ref: str) -> bool:
|
||||
"""Skip URLs, bare filenames, and template placeholders."""
|
||||
if ref.startswith(("http://", "https://")):
|
||||
return True
|
||||
if "/" not in ref:
|
||||
return True
|
||||
return any(c in ref for c in "{<*")
|
||||
|
||||
|
||||
class MetadataEnumChecker(BaseChecker):
|
||||
"""Generic checker for metadata enum fields."""
|
||||
|
||||
@@ -2001,6 +2057,7 @@ class SkillValidator:
|
||||
ReferencesDirectoryChecker(),
|
||||
ReferenceFileCountChecker(),
|
||||
NonStandardHeadersChecker(),
|
||||
ReferencePathChecker(),
|
||||
]
|
||||
|
||||
# Filter by category if specified
|
||||
|
||||
@@ -961,7 +961,7 @@ async def create_user(user: UserCreate, db: AsyncSession):
|
||||
## Cross-Reference
|
||||
|
||||
For comprehensive migration strategies and modernization patterns:
|
||||
- **Legacy Modernizer**: `/skills/legacy-modernizer/references/migration-strategies.md`
|
||||
- **Legacy Modernizer**: `../legacy-modernizer/references/migration-strategies.md`
|
||||
- Strangler pattern implementation
|
||||
- Feature flag strategies
|
||||
- Rollback procedures
|
||||
|
||||
Reference in New Issue
Block a user